/*-------------------------------------------------------------------------
 *
 * vacuumlazy.c
 *	  并发（“惰性”）清理。
 *
 * 清理的主要空间使用是要从索引中删除的已删除 TID 的数组存储。
 * 我们希望确保我们能够清理即使是最大的关系，同时保持有限的内存
 * 使用。为了做到这一点，我们对我们能同时跟踪的 TID 数量设置上限。
 *
 * 我们最多愿意使用 maintenance_work_mem（或许
 * autovacuum_work_mem）内存空间来跟踪已删除的 TID。我们最初
 * 分配一个大小为该值的 TID 数组，大小上限取决于表的大小
 * （此上限确保我们不会为清理小表分配巨大的区域）。如果
 * 数组威胁要溢出，我们必须调用 lazy_vacuum 来清理索引
 * （并清理我们已修剪的页面）。这释放了用于存储已删除 TID 的
 * 内存空间。
 *
 * 实际上，VACUUM 通常会在目标堆关系上完成初始遍历，而
 * 从未耗尽用于存储 TID 的空间。这意味着在完成初始遍历后
 * 只需要调用一次 lazy_vacuum。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/heap/vacuumlazy.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <math.h>

#include "access/amapi.h"
#include "access/genam.h"
#include "access/heapam.h"
#include "access/heapam_xlog.h"
#include "access/htup_details.h"
#include "access/multixact.h"
#include "access/transam.h"
#include "access/visibilitymap.h"
#include "access/xact.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "catalog/index.h"
#include "catalog/storage.h"
#include "commands/dbcommands.h"
#include "commands/progress.h"
#include "commands/vacuum.h"
#include "executor/instrument.h"
#include "miscadmin.h"
#include "optimizer/paths.h"
#include "pgstat.h"
#include "portability/instr_time.h"
#include "postmaster/autovacuum.h"
#include "storage/bufmgr.h"
#include "storage/freespace.h"
#include "storage/lmgr.h"
#include "tcop/tcopprot.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/pg_rusage.h"
#include "utils/timestamp.h"


/*
 * 空间/时间权衡参数：这些需要用户调节吗？
 *
 * 考虑截断关系时，我们希望至少有 REL_TRUNCATE_MINIMUM 或
 * (relsize / REL_TRUNCATE_FRACTION)（以较小者为准）潜在可释放的页面。
 */
#define REL_TRUNCATE_MINIMUM	1000
#define REL_TRUNCATE_FRACTION	16

/*
 * 截断锁定启发式的时间参数。
 *
 * 这些没有被作为用户可调的GUC值暴露，因为改善的潜力似乎不足以
 * 值得支持它们的成本。
 */
#define VACUUM_TRUNCATE_LOCK_CHECK_INTERVAL		20	/* 毫秒 */
#define VACUUM_TRUNCATE_LOCK_WAIT_INTERVAL		50	/* 毫秒 */
#define VACUUM_TRUNCATE_LOCK_TIMEOUT			5000	/* 毫秒 */

/*
 * 控制我们是否跳过索引清理和堆清理作为一种优化的阈值
 */
#define BYPASS_THRESHOLD_PAGES	0.02	/* 即 2% 的 rel_pages */

/*
 * 在堆扫描过程中每 4GB 执行一次安全性检查， approximately
 */
#define FAILSAFE_EVERY_PAGES \
	((BlockNumber) (((uint64) 4 * 1024 * 1024 * 1024) / BLCKSZ))

/*
 * 当一个表没有索引时，approximately 每 8GB 清理一次 FSM 
 *（不会完全准确，因为我们仅在处理具有可移除元组的堆页面之后清理 FSM）。
 * 当有索引时，将被忽略，并且在每次索引/堆清理过程中清理 FSM。
 */
#define VACUUM_FSM_EVERY_PAGES \
	((BlockNumber) (((uint64) 8 * 1024 * 1024 * 1024) / BLCKSZ))

/*
 * 在我们考虑跳过标记为干净的页面之前，必须至少见过这么多干净页面。
 */
#define SKIP_PAGES_THRESHOLD	((BlockNumber) 32)

/*
 * 延迟清理向后截断扫描的预取窗口大小。
 * 需要是 2 的幂。
 */
#define PREFETCH_SIZE			((BlockNumber) 32)

/*
 * 检查我们是否在并行清理中的宏。 如果为真，我们处于并行模式，且 DSM 段已初始化。
 */
#define ParallelVacuumIsActive(vacrel) ((vacrel)->pvs != NULL)

/* 报告错误上下文的清理阶段。 */
typedef enum
{
	VACUUM_ERRCB_PHASE_UNKNOWN,
	VACUUM_ERRCB_PHASE_SCAN_HEAP,
	VACUUM_ERRCB_PHASE_VACUUM_INDEX,
	VACUUM_ERRCB_PHASE_VACUUM_HEAP,
	VACUUM_ERRCB_PHASE_INDEX_CLEANUP,
	VACUUM_ERRCB_PHASE_TRUNCATE
} VacErrPhase;

typedef struct LVRelState
{
	/* 目标堆关系及其索引 */
	Relation	rel;
	Relation   *indrels;
	int			nindexes;

	/* 激进的 VACUUM？（必须设置 relfrozenxid >= FreezeLimit） */
	bool		aggressive;
	/* 使用可见性地图跳过？（由 DISABLE_PAGE_SKIPPING 禁用） */
	bool		skipwithvm;
	/* 回绕安全性已触发？ */
	bool		failsafe_active;
	/* 考虑跳过索引清理优化？ */
	bool		consider_bypass_optimization;

	/* 正在进行索引清理、索引清理、关系截断？ */
	bool		do_index_vacuuming;
	bool		do_index_cleanup;
	bool		do_rel_truncate;

	/* 缓冲区访问策略和并行清理状态 */
	BufferAccessStrategy bstrategy;
	ParallelVacuumState *pvs;

	/* rel 的初始 relfrozenxid 和 relminmxid */
	TransactionId relfrozenxid;
	MultiXactId relminmxid;
	double		old_live_tuples;	/* pg_class.reltuples 的上一个值 */

	/* VACUUM 操作冷冻和修剪的截止值 */
	TransactionId OldestXmin;
	GlobalVisState *vistest;
	/* VACUUM 操作冷冻 XIDs 和 MultiXactIds 的目标截止值 */
	TransactionId FreezeLimit;
	MultiXactId MultiXactCutoff;
	/* 跟踪设置 relfrozenxid/relminmxid 的最旧现存 XID/MXID */
	TransactionId NewRelfrozenXid;
	MultiXactId NewRelminMxid;
	bool		skippedallvis;

	/* 错误报告状态 */
	char	   *relnamespace;
	char	   *relname;
	char	   *indname;		/* 当前索引名称 */
	BlockNumber blkno;			/* 仅用于堆操作 */
	OffsetNumber offnum;		/* 仅用于堆操作 */
	VacErrPhase phase;
	bool		verbose;		/* VACUUM 详细模式？ */

	/*
	 * dead_items 存储被索引清理删除的索引元组的 TID。
	 * 每个 TID 指向一个来自通过 lazy_scan_prune 处理的堆页面的 LP_DEAD 行指针。
	 * lazy_vacuum_heap_rel 也需要该信息，它在第二次堆处理时将
	 * 相同的 LP_DEAD 行指针标记为 LP_UNUSED。
	 */
	VacDeadItems *dead_items;	/* 将删除的索引元组的 TIDs */
	BlockNumber rel_pages;		/* 页面总数 */
	BlockNumber scanned_pages;	/* 被检查的页面数量（未通过 VM 跳过） */
	BlockNumber removed_pages;	/* 通过关系截断移除的页面数量 */
	BlockNumber lpdead_item_pages;	/* # 包含 LP_DEAD 项的页面 */
	BlockNumber missed_dead_pages;	/* # 丢失的死元组的页面 */
	BlockNumber nonempty_pages; /* 实际上，最后一个非空页面 + 1 */

	/* 我们为表输出的统计信息 */
	double		new_rel_tuples; /* 新估计的元组总数 */
	double		new_live_tuples;	/* 新估计的活元组总数 */
	/* 索引 AMs 输出的统计信息 */
	IndexBulkDeleteResult **indstats;

	/* instrumentation 计数器 */
	int			num_index_scans;
	/* 随后的计数器仅适用于 scanned_pages */
	int64		tuples_deleted; /* 从表中删除的数量 */
	int64		lpdead_items;	/* 从索引中删除的数量 */
	int64		live_tuples;	/* 剩余的活元组数量 */
	int64		recently_dead_tuples;	/* 死元组，但尚未可清除 */
	int64		missed_dead_tuples; /* 可清除，但尚未删除 */
} LVRelState;

/*
 * lazy_scan_prune() 返回的状态
 */
typedef struct LVPagePruneState
{
	bool		hastup;			/* 页面是否阻止了 rel 截断？ */
	bool		has_lpdead_items;	/* 包含现有的 LP_DEAD 项 */

	/*
	 * 状态描述在修剪和冻结后应为页面设置的正确 VM 位状态。
	 * all_visible 意味着 !has_lpdead_items，但除非 all_visible 也设置为 true，否则
	 * 不要信任 all_frozen 结果。
	 */
	bool		all_visible;	/* 每个项目对所有人可见吗？ */
	bool		all_frozen;		/* 前提是 all_visible 也为 true */
	TransactionId visibility_cutoff_xid;	/* 用于恢复冲突 */
} LVPagePruneState;

/* 保存和恢复清理错误信息的结构。 */
typedef struct LVSavedErrInfo
{
	BlockNumber blkno;
	OffsetNumber offnum;
	VacErrPhase phase;
} LVSavedErrInfo;


/* 非导出函数原型 */
static void fc_lazy_scan_heap(LVRelState *fc_vacrel);
static BlockNumber fc_lazy_scan_skip(LVRelState *fc_vacrel, Buffer *fc_vmbuffer,
								  BlockNumber fc_next_block,
								  bool *fc_next_unskippable_allvis,
								  bool *fc_skipping_current_range);
static bool fc_lazy_scan_new_or_empty(LVRelState *fc_vacrel, Buffer fc_buf,
								   BlockNumber fc_blkno, Page fc_page,
								   bool fc_sharelock, Buffer fc_vmbuffer);
static void fc_lazy_scan_prune(LVRelState *fc_vacrel, Buffer fc_buf,
							BlockNumber fc_blkno, Page fc_page,
							LVPagePruneState *fc_prunestate);
static bool fc_lazy_scan_noprune(LVRelState *fc_vacrel, Buffer fc_buf,
							  BlockNumber fc_blkno, Page fc_page,
							  bool *fc_hastup, bool *fc_recordfreespace);
static void fc_lazy_vacuum(LVRelState *fc_vacrel);
static bool fc_lazy_vacuum_all_indexes(LVRelState *fc_vacrel);
static void fc_lazy_vacuum_heap_rel(LVRelState *fc_vacrel);
static int	fc_lazy_vacuum_heap_page(LVRelState *fc_vacrel, BlockNumber fc_blkno,
								  Buffer fc_buffer, int fc_index, Buffer *fc_vmbuffer);
static bool fc_lazy_check_wraparound_failsafe(LVRelState *fc_vacrel);
static void fc_lazy_cleanup_all_indexes(LVRelState *fc_vacrel);
static IndexBulkDeleteResult *fc_lazy_vacuum_one_index(Relation fc_indrel,
													IndexBulkDeleteResult *fc_istat,
													double fc_reltuples,
													LVRelState *fc_vacrel);
static IndexBulkDeleteResult *fc_lazy_cleanup_one_index(Relation fc_indrel,
													 IndexBulkDeleteResult *fc_istat,
													 double fc_reltuples,
													 bool fc_estimated_count,
													 LVRelState *fc_vacrel);
static bool fc_should_attempt_truncation(LVRelState *fc_vacrel);
static void fc_lazy_truncate_heap(LVRelState *fc_vacrel);
static BlockNumber fc_count_nondeletable_pages(LVRelState *fc_vacrel,
											bool *fc_lock_waiter_detected);
static void fc_dead_items_alloc(LVRelState *fc_vacrel, int fc_nworkers);
static void fc_dead_items_cleanup(LVRelState *fc_vacrel);
static bool fc_heap_page_is_all_visible(LVRelState *fc_vacrel, Buffer fc_buf,
									 TransactionId *fc_visibility_cutoff_xid, bool *fc_all_frozen);
static void fc_update_relstats_all_indexes(LVRelState *fc_vacrel);
static void fc_vacuum_error_callback(void *fc_arg);
static void fc_update_vacuum_error_info(LVRelState *fc_vacrel,
									 LVSavedErrInfo *fc_saved_vacrel,
									 int fc_phase, BlockNumber fc_blkno,
									 OffsetNumber fc_offnum);
static void fc_restore_vacuum_error_info(LVRelState *fc_vacrel,
									  const LVSavedErrInfo *fc_saved_vacrel);


/*
 *	heap_vacuum_rel() -- 对一个堆关系执行 VACUUM
 *
 *		此例程为 lazy_scan_heap 设置参数，然后调用 lazy_scan_heap，
 *		几乎所有工作都在这里进行。在调用返回后通过管理关系截断和更新
 *		rel 的 pg_class 条目来最终化所有内容。（还更新任何需要的索引的 pg_class 条目。）
 *
 *		在入口时，我们已经建立了一个事务并打开并锁定了关系。
 */
void heap_vacuum_rel(Relation fc_rel, VacuumParams *fc_params,
				BufferAccessStrategy fc_bstrategy)
{
	LVRelState *fc_vacrel;
	bool		fc_verbose,
				fc_instrument,
				fc_aggressive,
				fc_skipwithvm,
				fc_frozenxid_updated,
				fc_minmulti_updated;
	TransactionId fc_OldestXmin,
				fc_FreezeLimit;
	MultiXactId fc_OldestMxact,
				fc_MultiXactCutoff;
	BlockNumber fc_orig_rel_pages,
				fc_new_rel_pages,
				fc_new_rel_allvisible;
	PGRUsage	fc_ru0;
	TimestampTz fc_starttime = 0;
	PgStat_Counter fc_startreadtime = 0,
				fc_startwritetime = 0;
	WalUsage	fc_startwalusage = pgWalUsage;
	BufferUsage fc_startbufferusage = pgBufferUsage;
	ErrorContextCallback fc_errcallback;
	char	  **fc_indnames = NULL;

	fc_verbose = (fc_params->options & VACOPT_VERBOSE) != 0;
	fc_instrument = (fc_verbose || (IsAutoVacuumWorkerProcess() &&
							  fc_params->log_min_duration >= 0));
	if (fc_instrument)
	{
		pg_rusage_init(&fc_ru0);
		fc_starttime = GetCurrentTimestamp();
		if (track_io_timing)
		{
			fc_startreadtime = pgStatBlockReadTime;
			fc_startwritetime = pgStatBlockWriteTime;
		}
	}

	pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM,
								  RelationGetRelid(fc_rel));

	/*
	 * 获取 OldestXmin 截断，用于确定哪些已删除元组被视为 DEAD
	 * ，而不仅仅是 RECENTLY_DEAD。还获取相关的截断
	 * 用于确定哪些 XIDs/MultiXactIds 将被冻结。如果这是
	 * 一个激进的 VACUUM，则 lazy_scan_heap 不能留下未冻结的
	 * XIDs < FreezeLimit（所有 MXIDs < MultiXactCutoff 也需要被删除）。
	 */
	fc_aggressive = vacuum_set_xid_limits(fc_rel,
									   fc_params->freeze_min_age,
									   fc_params->freeze_table_age,
									   fc_params->multixact_freeze_min_age,
									   fc_params->multixact_freeze_table_age,
									   &fc_OldestXmin, &fc_OldestMxact,
									   &fc_FreezeLimit, &fc_MultiXactCutoff);

	fc_skipwithvm = true;
	if (fc_params->options & VACOPT_DISABLE_PAGE_SKIPPING)
	{
		/*
		 * 强制激进模式，并禁用使用可见性映射跳过块（即使那些设置为全冻结的）
		 */
		fc_aggressive = true;
		fc_skipwithvm = false;
	}

	/*
	 * 首先设置 ereport() 的错误追踪支持。其思想是在
	 * vacuum 期间设置错误上下文回调，以显示任何错误的附加信息。
	 * 在 vacuum 的不同阶段，我们更新状态，以便错误上下文回调始终显示
	 * 当前信息。
	 *
	 * 将堆关系的名称复制到本地内存中以用于错误报告
	 * 目的。并不总是安全假设我们可以获得每个关系的名称。
	 * 在 lazy_scan_heap 中，这些临时副本总是方便的。
	 */
	fc_vacrel = (LVRelState *) palloc0(sizeof(LVRelState));
	fc_vacrel->relnamespace = get_namespace_name(RelationGetNamespace(fc_rel));
	fc_vacrel->relname = pstrdup(RelationGetRelationName(fc_rel));
	fc_vacrel->indname = NULL;
	fc_vacrel->phase = VACUUM_ERRCB_PHASE_UNKNOWN;
	fc_vacrel->verbose = fc_verbose;
	fc_errcallback.callback = fc_vacuum_error_callback;
	fc_errcallback.arg = fc_vacrel;
	fc_errcallback.previous = error_context_stack;
	error_context_stack = &fc_errcallback;
	if (fc_verbose)
	{
		Assert(!IsAutoVacuumWorkerProcess());
		if (fc_aggressive)
			ereport(INFO,
					(errmsg("aggressively vacuuming \"%s.%s.%s\"",
							get_database_name(MyDatabaseId),
							fc_vacrel->relnamespace, fc_vacrel->relname)));
		else
			ereport(INFO,
					(errmsg("vacuuming \"%s.%s.%s\"",
							get_database_name(MyDatabaseId),
							fc_vacrel->relnamespace, fc_vacrel->relname)));
	}

	/* 设置有关关系及其索引的高级内容 */
	fc_vacrel->rel = fc_rel;
	vac_open_indexes(fc_vacrel->rel, RowExclusiveLock, &fc_vacrel->nindexes,
					 &fc_vacrel->indrels);
	if (fc_instrument && fc_vacrel->nindexes > 0)
	{
		/* 复制由 instrumentation 使用的索引名称（不是错误报告） */
		fc_indnames = palloc(sizeof(char *) * fc_vacrel->nindexes);
		for (int fc_i = 0; fc_i < fc_vacrel->nindexes; fc_i++)
			fc_indnames[fc_i] = pstrdup(RelationGetRelationName(fc_vacrel->indrels[fc_i]));
	}

	/*
	 * index_cleanup 参数要么禁用索引清理，要么在我们本会应用索引绕过优化时强制执行。默认值为'auto'，这将最终决策留给 lazy_vacuum()。
	 *
	 * truncate 参数允许用户避免尝试进行关系截断，尽管它不能强制执行截断。
	 */
	Assert(fc_params->index_cleanup != VACOPTVALUE_UNSPECIFIED);
	Assert(fc_params->truncate != VACOPTVALUE_UNSPECIFIED &&
		   fc_params->truncate != VACOPTVALUE_AUTO);
	fc_vacrel->aggressive = fc_aggressive;
	fc_vacrel->skipwithvm = fc_skipwithvm;
	fc_vacrel->failsafe_active = false;
	fc_vacrel->consider_bypass_optimization = true;
	fc_vacrel->do_index_vacuuming = true;
	fc_vacrel->do_index_cleanup = true;
	fc_vacrel->do_rel_truncate = (fc_params->truncate != VACOPTVALUE_DISABLED);
	if (fc_params->index_cleanup == VACOPTVALUE_DISABLED)
	{
		/* 强制预先禁用索引清理 */
		fc_vacrel->do_index_vacuuming = false;
		fc_vacrel->do_index_cleanup = false;
	}
	else if (fc_params->index_cleanup == VACOPTVALUE_ENABLED)
	{
		/* 强制索引清理。注意，故障保护仍然可以绕过。 */
		fc_vacrel->consider_bypass_optimization = false;
	}
	else
	{
		/* 默认/自动，动态做出所有决策 */
		Assert(fc_params->index_cleanup == VACOPTVALUE_AUTO);
	}

	fc_vacrel->bstrategy = fc_bstrategy;
	fc_vacrel->relfrozenxid = fc_rel->rd_rel->relfrozenxid;
	fc_vacrel->relminmxid = fc_rel->rd_rel->relminmxid;
	fc_vacrel->old_live_tuples = fc_rel->rd_rel->reltuples;

	/* 明确初始化页面计数器（保持整洁） */
	fc_vacrel->scanned_pages = 0;
	fc_vacrel->removed_pages = 0;
	fc_vacrel->lpdead_item_pages = 0;
	fc_vacrel->missed_dead_pages = 0;
	fc_vacrel->nonempty_pages = 0;
	/* dead_items_alloc 稍后分配 vacrel->dead_items */

	/* 分配/初始化输出统计状态 */
	fc_vacrel->new_rel_tuples = 0;
	fc_vacrel->new_live_tuples = 0;
	fc_vacrel->indstats = (IndexBulkDeleteResult **)
		palloc0(fc_vacrel->nindexes * sizeof(IndexBulkDeleteResult *));

	/* 初始化剩余计数器（保持整洁） */
	fc_vacrel->num_index_scans = 0;
	fc_vacrel->tuples_deleted = 0;
	fc_vacrel->lpdead_items = 0;
	fc_vacrel->live_tuples = 0;
	fc_vacrel->recently_dead_tuples = 0;
	fc_vacrel->missed_dead_tuples = 0;

	/*
	 * 确定我们将在 lazy_scan_heap 中扫描的区块范围，并最终确定在 lazy_scan_prune 中用于冻结和修剪的截止点。
	 *
	 * 我们期望 vistest 始终让 heap_page_prune 删除任何 xmax 小于 OldestXmin 的删除元组。lazy_scan_prune 绝不能对元组是否应被冻结或删除感到困惑。 （将来我们可能希望教 lazy_scan_prune 不时重新计算 vistest，以增加可以修剪掉的死元组数量。）
	 *
	 * 我们必须在确定 OldestXmin 之后确定 rel_pages。lazy_scan_heap 的物理堆扫描（对 rel_pages 小于页面的扫描）从而保证不会遗漏任何 XIDs 小于 OldestXmin 的元组。这些 XIDs 必须至少在扫描期间被考虑用于冻结（尽管不一定被冻结）。
	 */
	fc_vacrel->rel_pages = fc_orig_rel_pages = RelationGetNumberOfBlocks(fc_rel);
	fc_vacrel->OldestXmin = fc_OldestXmin;
	fc_vacrel->vistest = GlobalVisTestFor(fc_rel);
	/* FreezeLimit 控制 XID 冻结（始终 <= OldestXmin） */
	fc_vacrel->FreezeLimit = fc_FreezeLimit;
	/* MultiXactCutoff 控制 MXID 冻结（始终 <= OldestMxact） */
	fc_vacrel->MultiXactCutoff = fc_MultiXactCutoff;
	/* 初始化用于跟踪最旧的现存 XID/MXID 的状态 */
	fc_vacrel->NewRelfrozenXid = fc_OldestXmin;
	fc_vacrel->NewRelminMxid = fc_OldestMxact;
	fc_vacrel->skippedallvis = false;

	/*
	 * 使用 dead_items_alloc 分配 dead_items 数组内存。这处理并行 VACUUM 初始化，作为分配用于 dead_items 的共享内存空间的一部分。 （但首先进行故障保护预检查，以确保当 relfrozenxid 已经危险老旧时，不会尝试并行 VACUUM。）
	 */
	fc_lazy_check_wraparound_failsafe(fc_vacrel);
	fc_dead_items_alloc(fc_vacrel, fc_params->nworkers);

	/*
	 * 调用 lazy_scan_heap 执行所有所需的堆修剪、索引清理和堆清理（以及相关处理）
	 */
	fc_lazy_scan_heap(fc_vacrel);

	/*
	 * 释放由 dead_items_alloc 管理的资源。这在必要时结束并行模式。
	 */
	fc_dead_items_cleanup(fc_vacrel);
	Assert(!IsInParallelMode());

	/*
	 * 更新 rel 的每个索引的 pg_class 条目（在适当的情况下）。
	 *
	 * 与后面更新 rel 的 pg_class 条目不同，这不是关键的。仅维护规划器使用的 relpages/reltuples 统计信息。
	 */
	if (fc_vacrel->do_index_cleanup)
		fc_update_relstats_all_indexes(fc_vacrel);

	/* 完成 rel 的索引 */
	vac_close_indexes(fc_vacrel->nindexes, fc_vacrel->indrels, NoLock);

	/* 可选地截断 rel */
	if (fc_should_attempt_truncation(fc_vacrel))
		fc_lazy_truncate_heap(fc_vacrel);

	/* 弹出错误上下文栈 */
	error_context_stack = fc_errcallback.previous;

	/* 报告我们现在正在进行最终清理 */
	pgstat_progress_update_param(PROGRESS_VACUUM_PHASE,
								 PROGRESS_VACUUM_PHASE_FINAL_CLEANUP);

	/*
	 * 准备更新 rel 的 pg_class 条目。
	 *
	 * 积极的 VACUUM 必须始终能够将 relfrozenxid 前进到一个
	 * 大于等于 FreezeLimit 的值，并将 relminmxid 前进到一个
	 * 大于等于 MultiXactCutoff 的值。非积极的 VACUUM 可以
	 * 以任何量前进，或者根本不前进。
	 */
	Assert(fc_vacrel->NewRelfrozenXid == fc_OldestXmin ||
		   TransactionIdPrecedesOrEquals(fc_aggressive ? fc_FreezeLimit :
										 fc_vacrel->relfrozenxid,
										 fc_vacrel->NewRelfrozenXid));
	Assert(fc_vacrel->NewRelminMxid == fc_OldestMxact ||
		   MultiXactIdPrecedesOrEquals(fc_aggressive ? fc_MultiXactCutoff :
									   fc_vacrel->relminmxid,
									   fc_vacrel->NewRelminMxid));
	if (fc_vacrel->skippedallvis)
	{
		/*
		 * 在选择跳过所有可见页面范围的非积极 VACUUM 中，
		 * 必须保持原始 relfrozenxid。跟踪新值的状态将会
		 * 因为我们跳过的页面而错过未冻结的 XIDs。
		 */
		Assert(!fc_aggressive);
		fc_vacrel->NewRelfrozenXid = InvalidTransactionId;
		fc_vacrel->NewRelminMxid = InvalidMultiXactId;
	}

	/*
	 * 为了安全，将 relallvisible 限制为不超过我们设置
	 * pg_class.relpages 的值。
	 */
	fc_new_rel_pages = fc_vacrel->rel_pages;	/* 在可能的 rel 截断之后 */
	visibilitymap_count(fc_rel, &fc_new_rel_allvisible, NULL);
	if (fc_new_rel_allvisible > fc_new_rel_pages)
		fc_new_rel_allvisible = fc_new_rel_pages;

	/*
	 * 现在实际更新 rel 的 pg_class 条目。
	 *
	 * 原则上，new_live_tuples 可能是 -1，表示我们（仍然）
	 * 不知道元组数量。实际上这不可能发生，因为我们
	 * 扫描每一个没有被跳过的页面，使用可见性图。
	 */
	vac_update_relstats(fc_rel, fc_new_rel_pages, fc_vacrel->new_live_tuples,
						fc_new_rel_allvisible, fc_vacrel->nindexes > 0,
						fc_vacrel->NewRelfrozenXid, fc_vacrel->NewRelminMxid,
						&fc_frozenxid_updated, &fc_minmulti_updated, false);

	/*
	 * 将结果报告给累积统计系统。
	 *
	 * 有意避免告诉统计系统由于 VACUUM 跳过索引和堆清理
	 * 而在表中残留的 LP_DEAD 项目。ANALYZE 将认为剩余的
	 * LP_DEAD 项目是死的 "元组"。在保护措施防止大量
	 * 堆清理的情况下，似乎不宜太快再次进行清理是个好主意。
	 */
	pgstat_report_vacuum(RelationGetRelid(fc_rel),
						 fc_rel->rd_rel->relisshared,
						 Max(fc_vacrel->new_live_tuples, 0),
						 fc_vacrel->recently_dead_tuples +
						 fc_vacrel->missed_dead_tuples);
	pgstat_progress_end_command();

	if (fc_instrument)
	{
		TimestampTz fc_endtime = GetCurrentTimestamp();

		if (fc_verbose || fc_params->log_min_duration == 0 ||
			TimestampDifferenceExceeds(fc_starttime, fc_endtime,
									   fc_params->log_min_duration))
		{
			long		fc_secs_dur;
			int			fc_usecs_dur;
			WalUsage	fc_walusage;
			BufferUsage fc_bufferusage;
			StringInfoData fc_buf;
			char	   *fc_msgfmt;
			int32		fc_diff;
			double		fc_read_rate = 0,
						fc_write_rate = 0;

			TimestampDifference(fc_starttime, fc_endtime, &fc_secs_dur, &fc_usecs_dur);
			memset(&fc_walusage, 0, sizeof(WalUsage));
			WalUsageAccumDiff(&fc_walusage, &pgWalUsage, &fc_startwalusage);
			memset(&fc_bufferusage, 0, sizeof(BufferUsage));
			BufferUsageAccumDiff(&fc_bufferusage, &pgBufferUsage, &fc_startbufferusage);

			initStringInfo(&fc_buf);
			if (fc_verbose)
			{
				/*
				 * 积极性已在专门的 VACUUM VERBOSE ereport 中报告过
				 */
				Assert(!fc_params->is_wraparound);
				fc_msgfmt = _("finished vacuuming \"%s.%s.%s\": index scans: %d\n");
			}
			else if (fc_params->is_wraparound)
			{
				/*
				 * 虽然 VACUUM 完全可以同时是 is_wraparound 和 !aggressive，
				 * 但这只是一个边缘情况 -- is_wraparound 暗示积极性。
				 * 针对这个边缘情况产生不同的输出，以防万一。
				 */
				if (fc_aggressive)
					fc_msgfmt = _("automatic aggressive vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n");
				else
					fc_msgfmt = _("automatic vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n");
			}
			else
			{
				if (fc_aggressive)
					fc_msgfmt = _("automatic aggressive vacuum of table \"%s.%s.%s\": index scans: %d\n");
				else
					fc_msgfmt = _("automatic vacuum of table \"%s.%s.%s\": index scans: %d\n");
			}
			appendStringInfo(&fc_buf, fc_msgfmt,
							 get_database_name(MyDatabaseId),
							 fc_vacrel->relnamespace,
							 fc_vacrel->relname,
							 fc_vacrel->num_index_scans);
			appendStringInfo(&fc_buf, _("pages: %u removed, %u remain, %u scanned (%.2f%% of total)\n"),
							 fc_vacrel->removed_pages,
							 fc_new_rel_pages,
							 fc_vacrel->scanned_pages,
							 fc_orig_rel_pages == 0 ? 100.0 :
							 100.0 * fc_vacrel->scanned_pages / fc_orig_rel_pages);
			appendStringInfo(&fc_buf,
							 _("tuples: %lld removed, %lld remain, %lld are dead but not yet removable\n"),
							 (long long) fc_vacrel->tuples_deleted,
							 (long long) fc_vacrel->new_rel_tuples,
							 (long long) fc_vacrel->recently_dead_tuples);
			if (fc_vacrel->missed_dead_tuples > 0)
				appendStringInfo(&fc_buf,
								 _("tuples missed: %lld dead from %u pages not removed due to cleanup lock contention\n"),
								 (long long) fc_vacrel->missed_dead_tuples,
								 fc_vacrel->missed_dead_pages);
			fc_diff = (int32) (ReadNextTransactionId() - fc_OldestXmin);
			appendStringInfo(&fc_buf,
							 _("removable cutoff: %u, which was %d XIDs old when operation ended\n"),
							 fc_OldestXmin, fc_diff);
			if (fc_frozenxid_updated)
			{
				fc_diff = (int32) (fc_vacrel->NewRelfrozenXid - fc_vacrel->relfrozenxid);
				appendStringInfo(&fc_buf,
								 _("new relfrozenxid: %u, which is %d XIDs ahead of previous value\n"),
								 fc_vacrel->NewRelfrozenXid, fc_diff);
			}
			if (fc_minmulti_updated)
			{
				fc_diff = (int32) (fc_vacrel->NewRelminMxid - fc_vacrel->relminmxid);
				appendStringInfo(&fc_buf,
								 _("new relminmxid: %u, which is %d MXIDs ahead of previous value\n"),
								 fc_vacrel->NewRelminMxid, fc_diff);
			}
			if (fc_vacrel->do_index_vacuuming)
			{
				if (fc_vacrel->nindexes == 0 || fc_vacrel->num_index_scans == 0)
					appendStringInfoString(&fc_buf, _("index scan not needed: "));
				else
					appendStringInfoString(&fc_buf, _("index scan needed: "));

				fc_msgfmt = _("%u pages from table (%.2f%% of total) had %lld dead item identifiers removed\n");
			}
			else
			{
				if (!fc_vacrel->failsafe_active)
					appendStringInfoString(&fc_buf, _("index scan bypassed: "));
				else
					appendStringInfoString(&fc_buf, _("index scan bypassed by failsafe: "));

				fc_msgfmt = _("%u pages from table (%.2f%% of total) have %lld dead item identifiers\n");
			}
			appendStringInfo(&fc_buf, fc_msgfmt,
							 fc_vacrel->lpdead_item_pages,
							 fc_orig_rel_pages == 0 ? 100.0 :
							 100.0 * fc_vacrel->lpdead_item_pages / fc_orig_rel_pages,
							 (long long) fc_vacrel->lpdead_items);
			for (int fc_i = 0; fc_i < fc_vacrel->nindexes; fc_i++)
			{
				IndexBulkDeleteResult *fc_istat = fc_vacrel->indstats[fc_i];

				if (!fc_istat)
					continue;

				appendStringInfo(&fc_buf,
								 _("index \"%s\": pages: %u in total, %u newly deleted, %u currently deleted, %u reusable\n"),
								 fc_indnames[fc_i],
								 fc_istat->num_pages,
								 fc_istat->pages_newly_deleted,
								 fc_istat->pages_deleted,
								 fc_istat->pages_free);
			}
			if (track_io_timing)
			{
				double		fc_read_ms = (double) (pgStatBlockReadTime - fc_startreadtime) / 1000;
				double		fc_write_ms = (double) (pgStatBlockWriteTime - fc_startwritetime) / 1000;

				appendStringInfo(&fc_buf, _("I/O timings: read: %.3f ms, write: %.3f ms\n"),
								 fc_read_ms, fc_write_ms);
			}
			if (fc_secs_dur > 0 || fc_usecs_dur > 0)
			{
				fc_read_rate = (double) BLCKSZ * (fc_bufferusage.shared_blks_read + fc_bufferusage.local_blks_read) /
					(1024 * 1024) / (fc_secs_dur + fc_usecs_dur / 1000000.0);
				fc_write_rate = (double) BLCKSZ * (fc_bufferusage.shared_blks_dirtied + fc_bufferusage.local_blks_dirtied) /
					(1024 * 1024) / (fc_secs_dur + fc_usecs_dur / 1000000.0);
			}
			appendStringInfo(&fc_buf, _("avg read rate: %.3f MB/s, avg write rate: %.3f MB/s\n"),
							 fc_read_rate, fc_write_rate);
			appendStringInfo(&fc_buf,
							 _("buffer usage: %lld hits, %lld misses, %lld dirtied\n"),
							 (long long) (fc_bufferusage.shared_blks_hit + fc_bufferusage.local_blks_hit),
							 (long long) (fc_bufferusage.shared_blks_read + fc_bufferusage.local_blks_read),
							 (long long) (fc_bufferusage.shared_blks_dirtied + fc_bufferusage.local_blks_dirtied));
			appendStringInfo(&fc_buf,
							 _("WAL usage: %lld records, %lld full page images, %llu bytes\n"),
							 (long long) fc_walusage.wal_records,
							 (long long) fc_walusage.wal_fpi,
							 (unsigned long long) fc_walusage.wal_bytes);
			appendStringInfo(&fc_buf, _("system usage: %s"), pg_rusage_show(&fc_ru0));

			ereport(fc_verbose ? INFO : LOG,
					(errmsg_internal("%s", fc_buf.data)));
			pfree(fc_buf.data);
		}
	}

	/* 清理索引统计信息和索引名称 */
	for (int fc_i = 0; fc_i < fc_vacrel->nindexes; fc_i++)
	{
		if (fc_vacrel->indstats[fc_i])
			pfree(fc_vacrel->indstats[fc_i]);

		if (fc_instrument)
			pfree(fc_indnames[fc_i]);
	}
}

/*
 *	lazy_scan_heap() -- VACUUM 的工作函数
 *
 *		该例程修剪堆中的每个页面，并考虑冻结剩余元组的需要，
 *		而不包括可以通过可见性映射跳过的页面。还执行 FSM 和可见性映射的相关维护。
 *		这些步骤都在对目标堆关系的初始遍历中进行。
 *
 *		还调用 lazy_vacuum_all_indexes 来清理索引，这主要
 *		包括删除指向在修剪后仍留在堆页面中的 LP_DEAD 项的索引元组。
 *		对堆的早期初始遍历将收集需要删除的索引元组的 TID。
 *
 *		最后，调用 lazy_vacuum_heap_rel 来清理堆页面，
 *		这主要包括将收集的 TID 数组中的 LP_DEAD 项标记为 LP_UNUSED。
 *		这必须在对堆进行第二次最终遍历时进行，以保持所有索引 AM 依赖的基本不变性：
 *		任何现存的索引元组不得包含指向堆中 LP_UNUSED 行指针的 TID。
 *		我们必须禁止行指针的过早回收，以避免索引扫描在回收后迷惑于哪个 TID 指向哪个元组。
 *		（实际上，当目标堆关系恰好没有索引时，这不是一个问题，这允许我们安全地将一次遍历策略作为一种优化）。
 *
 *		实际上，我们通常有足够的空间来容纳所有 TID，因此在对堆的初始遍历完全完成后，
 *		不需要调用 lazy_vacuum 多于一次。否则情况就复杂一些：
 *		我们对堆的“初始遍历”仅适用于在需要调用 lazy_vacuum 之前已经修剪的那些页面，
 *		而我们对堆的“最终遍历”仅清理这些相同的堆页面。
 *		但是，每次调用 lazy_vacuum 时我们都会完全处理索引，
 *		这使得在内存短缺时索引处理非常低效。
 */
static void fc_lazy_scan_heap(LVRelState *fc_vacrel)
{
	BlockNumber fc_rel_pages = fc_vacrel->rel_pages,
				fc_blkno,
				fc_next_unskippable_block,
				fc_next_failsafe_block = 0,
				fc_next_fsm_block_to_vacuum = 0;
	VacDeadItems *fc_dead_items = fc_vacrel->dead_items;
	Buffer		fc_vmbuffer = InvalidBuffer;
	bool		fc_next_unskippable_allvis,
				fc_skipping_current_range;
	const int	fc_initprog_index[] = {
		PROGRESS_VACUUM_PHASE,
		PROGRESS_VACUUM_TOTAL_HEAP_BLKS,
		PROGRESS_VACUUM_MAX_DEAD_TUPLES
	};
	int64		fc_initprog_val[3];

	/* 报告我们正在扫描堆，宣传块的总数 */
	fc_initprog_val[0] = PROGRESS_VACUUM_PHASE_SCAN_HEAP;
	fc_initprog_val[1] = fc_rel_pages;
	fc_initprog_val[2] = fc_dead_items->max_items;
	pgstat_progress_update_multi_param(3, fc_initprog_index, fc_initprog_val);

	/* 使用可见性映射设置可跳过块的初始范围 */
	fc_next_unskippable_block = fc_lazy_scan_skip(fc_vacrel, &fc_vmbuffer, 0,
											&fc_next_unskippable_allvis,
											&fc_skipping_current_range);
	for (fc_blkno = 0; fc_blkno < fc_rel_pages; fc_blkno++)
	{
		Buffer		fc_buf;
		Page		fc_page;
		bool		fc_all_visible_according_to_vm;
		LVPagePruneState fc_prunestate;

		if (fc_blkno == fc_next_unskippable_block)
		{
			/*
			 * 不能安全地跳过此页面。必须扫描该页面。
			 * 但必须先确定该页面之后的下一个可跳过范围。
			 */
			fc_all_visible_according_to_vm = fc_next_unskippable_allvis;
			fc_next_unskippable_block = fc_lazy_scan_skip(fc_vacrel, &fc_vmbuffer,
													fc_blkno + 1,
													&fc_next_unskippable_allvis,
													&fc_skipping_current_range);

			Assert(fc_next_unskippable_block >= fc_blkno + 1);
		}
		else
		{
			/* 总是扫描最后一个页面（可能需要设置 nonempty_pages） */
			Assert(fc_blkno < fc_rel_pages - 1);

			if (fc_skipping_current_range)
				continue;

			/* 当前范围太小，无法跳过 -- 直接扫描该页面 */
			fc_all_visible_according_to_vm = true;
		}

		fc_vacrel->scanned_pages++;

		/* 报告为已扫描块，更新错误回溯信息 */
		pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_SCANNED, fc_blkno);
		fc_update_vacuum_error_info(fc_vacrel, NULL, VACUUM_ERRCB_PHASE_SCAN_HEAP,
								 fc_blkno, InvalidOffsetNumber);

		vacuum_delay_point();

		/*
		 * 定期检查是否应触发循环绕回故障保护。
		 *
		 * 在 lazy_vacuum_all_indexes() 内部也有类似的检查，
		 * 但 relfrozenxid 可能在我们到达该点之前就开始看起来危险地老旧。
		 * 此检查还为一次性策略和将 index_cleanup 参数设置为 'off' 的两次遍历策略提供了故障保护覆盖。
		 */
		if (fc_blkno - fc_next_failsafe_block >= FAILSAFE_EVERY_PAGES)
		{
			fc_lazy_check_wraparound_failsafe(fc_vacrel);
			fc_next_failsafe_block = fc_blkno;
		}

		/*
		 * 考虑我们是否确实有足够的空间在页面上处理 TID
		 * 。如果我们接近超出可用的 dead_items TID 空间，暂停并进行一次 vacuuming 循环，然后再处理此页面。
		 */
		Assert(fc_dead_items->max_items >= MaxHeapTuplesPerPage);
		if (fc_dead_items->max_items - fc_dead_items->num_items < MaxHeapTuplesPerPage)
		{
			/*
			 * 在开始索引清理之前，我们释放在可见性映射页面上可能持有的任何锁定。
			 * 这对于正确性并不是必要的，但我们还是这样做，以避免在长时间的、不相关的操作中保持锁定。
			 */
			if (BufferIsValid(fc_vmbuffer))
			{
				ReleaseBuffer(fc_vmbuffer);
				fc_vmbuffer = InvalidBuffer;
			}

			/* 执行一轮索引和堆清理 */
			fc_vacrel->consider_bypass_optimization = false;
			fc_lazy_vacuum(fc_vacrel);

			/*
			 * 清理自由空间映射，以使新释放的空间在
			 * 上层 FSM 页面上可见。注意我们尚未处理 blkno。
			 */
			FreeSpaceMapVacuumRange(fc_vacrel->rel, fc_next_fsm_block_to_vacuum,
									fc_blkno);
			fc_next_fsm_block_to_vacuum = fc_blkno;

			/* 报告我们再次扫描堆 */
			pgstat_progress_update_param(PROGRESS_VACUUM_PHASE,
										 PROGRESS_VACUUM_PHASE_SCAN_HEAP);
		}

		/*
		 * 锁定可见性映射页面，以防我们需要将页面标记为全部可见。
		 * 在大多数情况下，这将非常便宜，因为我们反正已经锁定了正确的页面。
		 */
		visibilitymap_pin(fc_vacrel->rel, fc_blkno, &fc_vmbuffer);

		/* 完成准备检查。实际扫描页面。 */
		fc_buf = ReadBufferExtended(fc_vacrel->rel, MAIN_FORKNUM, fc_blkno,
								 RBM_NORMAL, fc_vacrel->bstrategy);
		fc_page = BufferGetPage(fc_buf);

		/*
		 * 我们需要一个缓冲区清理锁来修剪 HOT 链并进行懒惰扫描修剪时的页面碎片整理。
		 * 但是，当不可能立即获取清理锁时，我们可能能够通过使用 lazy_scan_noprune 来减少处理。
		 */
		if (!ConditionalLockBufferForCleanup(fc_buf))
		{
			bool		fc_hastup,
						fc_recordfreespace;

			LockBuffer(fc_buf, BUFFER_LOCK_SHARE);

			/* 在 lazy_scan_noprune 调用之前检查新页面或空页面 */
			if (fc_lazy_scan_new_or_empty(fc_vacrel, fc_buf, fc_blkno, fc_page, true,
									   fc_vmbuffer))
			{
				/* 作为新/空页面处理（锁定和锁定已释放） */
				continue;
			}

			/* 在 dead_items 数组中收集 LP_DEAD 项，计数元组 */
			if (fc_lazy_scan_noprune(fc_vacrel, fc_buf, fc_blkno, fc_page, &fc_hastup,
								  &fc_recordfreespace))
			{
				Size		fc_freespace = 0;

				/*
				 * 成功处理页面（无需清理锁）——只需进行关系修剪和 FSM 步骤，像
				 * lazy_scan_prune 情况一样。不必尝试匹配其
				 * 可见性映射设置步骤。
				 */
				if (fc_hastup)
					fc_vacrel->nonempty_pages = fc_blkno + 1;
				if (fc_recordfreespace)
					fc_freespace = PageGetHeapFreeSpace(fc_page);
				UnlockReleaseBuffer(fc_buf);
				if (fc_recordfreespace)
					RecordPageWithFreeSpace(fc_vacrel->rel, fc_blkno, fc_freespace);
				continue;
			}

			/*
			 * lazy_scan_noprune 无法完成所有必要的处理。等待
			 * 清理锁，并按照惯常方式调用 lazy_scan_prune。
			 */
			Assert(fc_vacrel->aggressive);
			LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);
			LockBufferForCleanup(fc_buf);
		}

		/* 在 lazy_scan_prune 调用之前检查新页面或空页面 */
		if (fc_lazy_scan_new_or_empty(fc_vacrel, fc_buf, fc_blkno, fc_page, false, fc_vmbuffer))
		{
			/* 作为新/空页面处理（锁定和锁定已释放） */
			continue;
		}

		/*
		 * 修剪、冻结并计数元组。
		 *
		 * 在 dead_items 数组中累积页面上剩余 LP_DEAD 行指针的详细信息。
		 * 这包括我们自己修剪的 LP_DEAD 行指针，以及早些时候修剪的现有 LP_DEAD 行指针。
		 * 还考虑在具有存储的剩余项的元组头中冻结 XID。
		 */
		fc_lazy_scan_prune(fc_vacrel, fc_buf, fc_blkno, fc_page, &fc_prunestate);

		Assert(!fc_prunestate.all_visible || !fc_prunestate.has_lpdead_items);

		/* 记住最后一个具有不可移除元组的页面的位置 */
		if (fc_prunestate.hastup)
			fc_vacrel->nonempty_pages = fc_blkno + 1;

		if (fc_vacrel->nindexes == 0)
		{
			/*
			 * 考虑在采用一次性策略时按页面进行堆清理的必要性。
			 *
			 * 一次性策略将永远不会调用 lazy_vacuum()。在这里执行的步骤可以视为
			 * 调用 lazy_vacuum() 的一次性等效。
			 */
			if (fc_prunestate.has_lpdead_items)
			{
				Size		fc_freespace;

				fc_lazy_vacuum_heap_page(fc_vacrel, fc_blkno, fc_buf, 0, &fc_vmbuffer);

				/* 忘记我们刚刚清理过的 LP_DEAD 项 */
				fc_dead_items->num_items = 0;

				/*
				 * 定期执行FSM清理以使新释放的
				 * 空间在上层FSM页面上可见。注意我们尚未
				 * 对blkno执行FSM处理。
				 */
				if (fc_blkno - fc_next_fsm_block_to_vacuum >= VACUUM_FSM_EVERY_PAGES)
				{
					FreeSpaceMapVacuumRange(fc_vacrel->rel, fc_next_fsm_block_to_vacuum,
											fc_blkno);
					fc_next_fsm_block_to_vacuum = fc_blkno;
				}

				/*
				 * 现在对blkno执行FSM处理，并继续下一个
				 * 页面。
				 *
				 * 我们对lazy_vacuum_heap_page()的调用将考虑是否
				 * 可以独立于lazy_scan_prune()设置all_visible/all_frozen。
				 * 注意prunestate已被lazy_vacuum_heap_page()调用使无效。
				 */
				fc_freespace = PageGetHeapFreeSpace(fc_page);

				UnlockReleaseBuffer(fc_buf);
				RecordPageWithFreeSpace(fc_vacrel->rel, fc_blkno, fc_freespace);
				continue;
			}

			/*
			 * 由于清理未遇到/创建任何需要清理的LP_DEAD项，
			 * 因此没有调用lazy_vacuum_heap_page()。清理状态尚未被
			 * 使无效，因此继续进行基于prunestate的可见性映射和FSM步骤
			 * （就像二次扫描策略）。
			 */
			Assert(fc_dead_items->num_items == 0);
		}

		/*
		 * 根据来自VM的信息（截至最后一次lazy_scan_skip()调用）和
		 * prunestate处理设置可见性映射位
		 */
		if (!fc_all_visible_according_to_vm && fc_prunestate.all_visible)
		{
			uint8		fc_flags = VISIBILITYMAP_ALL_VISIBLE;

			if (fc_prunestate.all_frozen)
				fc_flags |= VISIBILITYMAP_ALL_FROZEN;

			/*
			 * 在可见性映射页面被设置而页面级位被清除的情况下绝不应该发生，
			 * 但反向是允许的（如果校验和未启用）。无论如何，设置
			 * 两个位以便我们重新同步。
			 *
			 * 注意：如果堆页面是全可见的但VM位未设置，
			 * 我们不需要将堆页面标记为脏。然而，如果校验和
			 * 已启用，我们确实需要确保在将其传递给visibilitymap_set()
			 * 之前将堆页面标记为脏，因为它可能会被记录。
			 * 鉴于这种情况只会在崩溃后发生罕见情况，因此不值得优化。
			 */
			PageSetAllVisible(fc_page);
			MarkBufferDirty(fc_buf);
			visibilitymap_set(fc_vacrel->rel, fc_blkno, fc_buf, InvalidXLogRecPtr,
							  fc_vmbuffer, fc_prunestate.visibility_cutoff_xid,
							  fc_flags);
		}

		/*
		 * 自PostgreSQL 9.2以来，如果页面级位被清除，
		 * 可见性映射位绝不应该被设置。然而，在调用lazy_scan_skip()
		 * 之后，这个位可能被清除，因此在得出VM已损坏之前，我们
		 * 必须在缓冲区锁定之前重新检查。
		 */
		else if (fc_all_visible_according_to_vm && !PageIsAllVisible(fc_page)
				 && VM_ALL_VISIBLE(fc_vacrel->rel, fc_blkno, &fc_vmbuffer))
		{
			elog(WARNING, "page is not marked all-visible but visibility map bit is set in relation \"%s\" page %u",
				 fc_vacrel->relname, fc_blkno);
			visibilitymap_clear(fc_vacrel->rel, fc_blkno, fc_vmbuffer,
								VISIBILITYMAP_VALID_BITS);
		}

		/*
		 * GetOldestNonRemovableTransactionId()返回的值有可能向后移动，
		 * 因此我们看到的元组似乎仍未对所有人可见，而PD_ALL_VISIBLE
		 * 已经设置并不是错误。实际安全的xmin值不会向后移动，但
		 * GetOldestNonRemovableTransactionId()是保守的，有时返回的值
		 * 小得不必要，因此如果我们看到这种矛盾，它只是意味着我们认为
		 * 尚未对所有人可见的元组实际上是可见的，PD_ALL_VISIBLE标志
		 * 是正确的。
		 *
		 * 然而，页面上不应该有设置了PD_ALL_VISIBLE的LP_DEAD项。
		 */
		else if (fc_prunestate.has_lpdead_items && PageIsAllVisible(fc_page))
		{
			elog(WARNING, "page containing LP_DEAD items is marked as all-visible in relation \"%s\" page %u",
				 fc_vacrel->relname, fc_blkno);
			PageClearAllVisible(fc_page);
			MarkBufferDirty(fc_buf);
			visibilitymap_clear(fc_vacrel->rel, fc_blkno, fc_vmbuffer,
								VISIBILITYMAP_VALID_BITS);
		}

		/*
		 * 如果全可见页面是全冻结的但尚未标记为全冻结，
		 * 则将其标记为全冻结。注意，只有在all_visible为真时，
		 * all_frozen才有效，因此我们必须检查两个prunestate字段。
		 */
		else if (fc_all_visible_according_to_vm && fc_prunestate.all_visible &&
				 fc_prunestate.all_frozen &&
				 !VM_ALL_FROZEN(fc_vacrel->rel, fc_blkno, &fc_vmbuffer))
		{
			/*
			 * 我们可以将 InvalidTransactionId 作为此处的截止 XID，
			 * 因为设置 all-frozen 位不会导致恢复冲突。
			 */
			visibilitymap_set(fc_vacrel->rel, fc_blkno, fc_buf, InvalidXLogRecPtr,
							  fc_vmbuffer, InvalidTransactionId,
							  VISIBILITYMAP_ALL_FROZEN);
		}

		/*
		 * 块的最后步骤：释放清理锁，记录 FSM 中的空闲空间
		 */
		if (fc_prunestate.has_lpdead_items && fc_vacrel->do_index_vacuuming)
		{
			/*
			 * 等待 lazy_vacuum_heap_rel() 来保存空闲空间。 这不仅能节省一些资源；
			 * 还允许我们记录 lazy_vacuum_heap_page() 在能够截断页面的行指针数组的情况下
			 * 将提供的任何额外空闲空间。
			 *
			 * 注意：实际上并不能 100% 确定我们一定会调用 lazy_vacuum_heap_rel()——
			 * lazy_vacuum() 可能会选择跳过索引清理（因此必须跳过堆清理）。 这被认为是可以的，
			 * 因为这仅仅发生在紧急情况下，或者当空闲空间非常少的情况下。（此外，
			 * 一旦索引清理被放弃，我们就会开始在 FSM 中记录空闲空间。）
			 *
			 * 注意：一遍（没有索引）情况仅在修剪过程中没有 LP_DEAD 项时才应进行到这一步。
			 */
			Assert(fc_vacrel->nindexes > 0);
			UnlockReleaseBuffer(fc_buf);
		}
		else
		{
			Size		fc_freespace = PageGetHeapFreeSpace(fc_page);

			UnlockReleaseBuffer(fc_buf);
			RecordPageWithFreeSpace(fc_vacrel->rel, fc_blkno, fc_freespace);
		}
	}

	fc_vacrel->blkno = InvalidBlockNumber;
	if (BufferIsValid(fc_vmbuffer))
		ReleaseBuffer(fc_vmbuffer);

	/* 报告现在所有内容都已扫描 */
	pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_SCANNED, fc_blkno);

	/* 现在我们可以计算 pg_class.reltuples 的新值 */
	fc_vacrel->new_live_tuples = vac_estimate_reltuples(fc_vacrel->rel, fc_rel_pages,
													 fc_vacrel->scanned_pages,
													 fc_vacrel->live_tuples);

	/*
	 * 还计算幸存堆条目的总数。在 (不太可能的) 情况下新_live_tuples 为 -1，
	 * 将其视为零。
	 */
	fc_vacrel->new_rel_tuples =
		Max(fc_vacrel->new_live_tuples, 0) + fc_vacrel->recently_dead_tuples +
		fc_vacrel->missed_dead_tuples;

	/*
	 * 进行索引清理（调用每个索引的 ambulkdelete 例程），然后进行相关的堆清理
	 */
	if (fc_dead_items->num_items > 0)
		fc_lazy_vacuum(fc_vacrel);

	/*
	 * 清理剩余的空闲空间映射。无论是否存在索引，以及是否绕过了索引清理，
	 * 我们都必须执行此操作。
	 */
	if (fc_blkno > fc_next_fsm_block_to_vacuum)
		FreeSpaceMapVacuumRange(fc_vacrel->rel, fc_next_fsm_block_to_vacuum, fc_blkno);

	/* 报告所有已清理的块 */
	pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_VACUUMED, fc_blkno);

	/* 进行最终的索引清理（调用每个索引的 amvacuumcleanup 例程） */
	if (fc_vacrel->nindexes > 0 && fc_vacrel->do_index_cleanup)
		fc_lazy_cleanup_all_indexes(fc_vacrel);
}

/*
 *	lazy_scan_skip() -- 使用可见性映射设置可跳过块的范围。
 *
 * lazy_scan_heap() 每次需要通过可见性映射设置新的可跳过块范围时都会调用此函数。
 * 调用者传递下一个块。我们为该范围返回一个 next_unskippable_block。
 * 当没有可跳过块时，我们只返回调用者的 next_block。
 * 返回块的全可见状态也在 *next_unskippable_allvis 中为调用者设置。
 * 块通常不会全可见（因为它是不可跳过的），但在激进的 VACUUM 期间
 * （以及在某些边缘情况下）它可能是可见的。
 *
 * 设置 *skipping_current_range 以指示调用者是否应跳过此范围。
 * 成本和收益驱动我们的决定。非常小的范围将不会被跳过。
 *
 * 注意：我们对哪些块可以被跳过的看法可能会立即过时。
 * 如果调用者“错过”了一个同时清除的全可见或全冻结标记的页面，
 * 这也是可以的。最重要的是调用者扫描所有可能包含 XIDs < OldestXmin
 * 或 MXIDs < OldestMxact 的元组的页面。
 * （实际上，非激进的 VACUUM 可以选择跳过具有较旧 XIDs/MXIDs 的全可见页面。
 * 当实际做出跳过此类范围的选择时，vacrel->skippedallvis 标志将在这里设置，
 * 使一切变得安全。）
 */
static BlockNumber fc_lazy_scan_skip(LVRelState *fc_vacrel, Buffer *fc_vmbuffer, BlockNumber fc_next_block,
			   bool *fc_next_unskippable_allvis, bool *fc_skipping_current_range)
{
	BlockNumber fc_rel_pages = fc_vacrel->rel_pages,
				fc_next_unskippable_block = fc_next_block,
				fc_nskippable_blocks = 0;
	bool		fc_skipsallvis = false;

	*fc_next_unskippable_allvis = true;
	while (fc_next_unskippable_block < fc_rel_pages)
	{
		uint8		fc_mapbits = visibilitymap_get_status(fc_vacrel->rel,
													   fc_next_unskippable_block,
													   fc_vmbuffer);

		if ((fc_mapbits & VISIBILITYMAP_ALL_VISIBLE) == 0)
		{
			Assert((fc_mapbits & VISIBILITYMAP_ALL_FROZEN) == 0);
			*fc_next_unskippable_allvis = false;
			break;
		}

		/*
		 * 调用者必须扫描最后一个页面以确定它是否有元组
		 * （调用者必须有机会设置 vacrel->nonempty_pages）。
		 * 这个规则避免了 lazy_truncate_heap() 在尝试截断时
		 * 占用排它锁，因为最后一个页面上有元组（其他临近页面
		 * 上也可能有元组，但那些可以跳过）。
		 *
		 * 通过始终将最后一个块视为不安全的跳过来实现这一点。
		 */
		if (fc_next_unskippable_block == fc_rel_pages - 1)
			break;

		/* DISABLE_PAGE_SKIPPING 使所有跳过操作变得不安全 */
		if (!fc_vacrel->skipwithvm)
			break;

		/*
		 * 激进的 VACUUM 调用者不能仅因为页面全可见而跳过页面。
		 * 它们仍然可以跳过全冻结页面，这些页面不能包含 XIDs < OldestXmin
		 * （这些 XIDs 现在还没有被冻结）。
		 */
		if ((fc_mapbits & VISIBILITYMAP_ALL_FROZEN) == 0)
		{
			if (fc_vacrel->aggressive)
				break;

			/*
			 * 在非激进的情况下，全可见块是安全的跳过。但是
			 * 请记住，最终范围包含这样的块以备后用。
			 */
			fc_skipsallvis = true;
		}

		vacuum_delay_point();
		fc_next_unskippable_block++;
		fc_nskippable_blocks++;
	}

	/*
	 * 我们只跳过至少有 SKIP_PAGES_THRESHOLD 个连续页面的范围。
	 * 由于我们是顺序读取，操作系统应该为我们做预读，
	 * 所以现在偶尔跳过一个页面是没有好处的。
	 * 跳过这样的范围甚至可能会阻碍顺序检测。
	 *
	 * 此测试还可以在非激进 VACUUM 期间更频繁地推进 relfrozenxid。
	 * 如果范围内有任何全可见页面，则跳过使更新 relfrozenxid
	 * 变得不安全，这确实是一个缺点。
	 */
	if (fc_nskippable_blocks < SKIP_PAGES_THRESHOLD)
		*fc_skipping_current_range = false;
	else
	{
		*fc_skipping_current_range = true;
		if (fc_skipsallvis)
			fc_vacrel->skippedallvis = true;
	}

	return fc_next_unskippable_block;
}

/*
 *	lazy_scan_new_or_empty() -- lazy_scan_heap() 新页/空页处理。
 *
 * 必须在调用 lazy_scan_prune 或 lazy_scan_noprune 之前在这里处理新的和空的页，因为它们未准备好处理
 * 新的或空的页。
 *
 * 有必要将新页视为特殊情况，因为维护可见性映射和 FSM 的规则在处理空页时稍有不同（尽管新页
 * 可以在重做截断期间被截断）。
 *
 * 空页并不是一个真正的特殊情况——它们只是没有分配元组的堆页（甚至包括 LP_UNUSED 项）。你可能
 * 会想知道为什么我们需要在这里处理它们。之所以有必要，仅仅是因为一个在堆关系扩展期间发生的
 * 硬崩溃的边缘情况。如果我们能够使关系扩展具有崩溃安全性，那么在这里处理空页（或者新页）
 * 就不再是必要的。
 *
 * 调用者必须至少持有共享锁。在这种情况下，我们可能需要提升锁，因此调用者持有的锁的类型
 * 需要通过 'sharelock' 参数进行指定。
 *
 * 在常见情况下返回 false，调用者应该继续调用 lazy_scan_prune（或 lazy_scan_noprune）。
 * 其他情况下返回 true，表示 lazy_scan_heap 已经完成对页面的处理，代表调用者释放锁。
 */
static bool fc_lazy_scan_new_or_empty(LVRelState *fc_vacrel, Buffer fc_buf, BlockNumber fc_blkno,
					   Page fc_page, bool fc_sharelock, Buffer fc_vmbuffer)
{
	Size		fc_freespace;

	if (PageIsNew(fc_page))
	{
		/*
		 * 如果后台通过单个页扩展关系，但在新初始化的页被写出之前崩溃，或者在批量扩展关系时
		 * （这会在关系的末尾创建多个空页），然后将它们进入 FSM，那么可能会留下全零页。
		 *
		 * 注意，我们没有将页面输入可见性映射。这的缺点是我们在后续清理中会重复访问此页，
		 * 但否则我们将永远无法在提升的备用机上发现空间。重复检查的危害通常不会太大。
		 * 空间通常应该在某个时刻被使用，否则就不会有定期的清理。
		 *
		 * 确保这些页面在 FSM 中，以确保它们可以被重用。通过测试是否有任何空间记录在该页上
		 * 来做到这一点。如果没有，则进入它。在释放对堆页的锁之后执行此操作，毕竟 FSM 是近似的。
		 */
		UnlockReleaseBuffer(fc_buf);

		if (GetRecordedFreeSpace(fc_vacrel->rel, fc_blkno) == 0)
		{
			fc_freespace = BLCKSZ - SizeOfPageHeaderData;

			RecordPageWithFreeSpace(fc_vacrel->rel, fc_blkno, fc_freespace);
		}

		return true;
	}

	if (PageIsEmpty(fc_page))
	{
		/*
		 * 看起来调用者总是能够在空页上获取清理锁。但是不要冒任何风险——升级为排他锁
		 * （不过仍然不需要清理锁）。
		 */
		if (fc_sharelock)
		{
			LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);
			LockBuffer(fc_buf, BUFFER_LOCK_EXCLUSIVE);

			if (!PageIsEmpty(fc_page))
			{
				/* 页面不是新页或空页——现在保持锁定并固定 */
				return false;
			}
		}
		else
		{
			/* 已经有一个完整的清理锁（这已经足够了） */
		}

		/*
		 * 与新页不同，空页总是设置为全部可见和全部冻结。
		 */
		if (!PageIsAllVisible(fc_page))
		{
			START_CRIT_SECTION();

			/* 在写入 WAL 记录之前标记缓冲区为脏 */
			MarkBufferDirty(fc_buf);

			/*
			 * 可能另一个后台扩展了堆，初始化了页面，然后由于一个错误未能将页面写入WAL日志。由于堆扩展没有写入WAL日志，恢复可能尝试重放我们将页面设置为全可见的记录，并发现页面未初始化，这将导致PANIC。为防止这种情况，检查页面是否之前已写入WAL日志，如果没有，现在就进行记录。
			 */
			if (RelationNeedsWAL(fc_vacrel->rel) &&
				PageGetLSN(fc_page) == InvalidXLogRecPtr)
				log_newpage_buffer(fc_buf, true);

			PageSetAllVisible(fc_page);
			visibilitymap_set(fc_vacrel->rel, fc_blkno, fc_buf, InvalidXLogRecPtr,
							  fc_vmbuffer, InvalidTransactionId,
							  VISIBILITYMAP_ALL_VISIBLE | VISIBILITYMAP_ALL_FROZEN);
			END_CRIT_SECTION();
		}

		fc_freespace = PageGetHeapFreeSpace(fc_page);
		UnlockReleaseBuffer(fc_buf);
		RecordPageWithFreeSpace(fc_vacrel->rel, fc_blkno, fc_freespace);
		return true;
	}

	/* 页面不是新创建的或为空 -- 保持锁定并固定 */
	return false;
}

/*
 *	lazy_scan_prune() -- lazy_scan_heap() 修剪和冻结。
 *
 * 调用者必须在缓冲区上保持固定和缓冲区清理锁。
 *
 * 在PostgreSQL 14之前，有非常少见的情况，heap_page_prune()可以与我们的HeapTupleSatisfiesVacuum()调用在元组是否应被视为DEAD上不一致。这发生在一个插入事务并发中止（在我们的heap_page_prune()调用之后，在我们的HeapTupleSatisfiesVacuum()调用之前）。为了处理对VACUUM来说是DEAD但在修剪后仍然保留存储的元组，我们遇到了相当多的复杂性。
 *
 * 我们现在采取的方法是在检测到竞争条件时重新启动修剪。这允许heap_page_prune()修剪现在已中止事务插入的元组。这有点粗糙，但它保证任何进入dead_items数组的项目都是简单的LP_DEAD行指针，并且每个具有元组存储的剩余项目都被视为冻结的候选者。
 */
static void fc_lazy_scan_prune(LVRelState *fc_vacrel,
				Buffer fc_buf,
				BlockNumber fc_blkno,
				Page fc_page,
				LVPagePruneState *fc_prunestate)
{
	Relation	fc_rel = fc_vacrel->rel;
	OffsetNumber fc_offnum,
				fc_maxoff;
	ItemId		fc_itemid;
	HeapTupleData fc_tuple;
	HTSV_Result fc_res;
	int			fc_tuples_deleted,
				fc_lpdead_items,
				fc_live_tuples,
				fc_recently_dead_tuples;
	int			fc_nnewlpdead;
	int			fc_nfrozen;
	TransactionId fc_NewRelfrozenXid;
	MultiXactId fc_NewRelminMxid;
	OffsetNumber fc_deadoffsets[MaxHeapTuplesPerPage];
	xl_heap_freeze_tuple fc_frozen[MaxHeapTuplesPerPage];

	Assert(BufferGetBlockNumber(fc_buf) == fc_blkno);

	/*
	 * maxoff在heap_page_prune中由于行指针数组截断可能会减少。我们可以安全地忽略，因为回收的空间在下面将继续看起来像LP_UNUSED项目。
	 */
	fc_maxoff = PageGetMaxOffsetNumber(fc_page);

retry:

	/* 初始化（或重置）页面级状态 */
	fc_NewRelfrozenXid = fc_vacrel->NewRelfrozenXid;
	fc_NewRelminMxid = fc_vacrel->NewRelminMxid;
	fc_tuples_deleted = 0;
	fc_lpdead_items = 0;
	fc_live_tuples = 0;
	fc_recently_dead_tuples = 0;

	/*
	 * 修剪此页面中的所有HOT更新链。
	 *
	 * 我们将通过修剪步骤移除的元组计数为tuples_deleted。其最终值可以视为已从表中删除的元组数量。它不应与lpdead_items混淆；lpdead_items的最终值可以视为从索引中删除的元组数量。
	 */
	fc_tuples_deleted = heap_page_prune(fc_rel, fc_buf, fc_vacrel->OldestXmin,
									 fc_vacrel->vistest,
									 InvalidTransactionId, 0, &fc_nnewlpdead,
									 &fc_vacrel->offnum);

	/*
	 * 现在扫描页面以收集LP_DEAD项目，并检查剩余存储元组中是否需要冻结的元组。
	 */
	fc_prunestate->hastup = false;
	fc_prunestate->has_lpdead_items = false;
	fc_prunestate->all_visible = true;
	fc_prunestate->all_frozen = true;
	fc_prunestate->visibility_cutoff_xid = InvalidTransactionId;
	fc_nfrozen = 0;

	for (fc_offnum = FirstOffsetNumber;
		 fc_offnum <= fc_maxoff;
		 fc_offnum = OffsetNumberNext(fc_offnum))
	{
		bool		fc_tuple_totally_frozen;

		/*
		 * 设置偏移量以便我们可以在处理此元组时显示它
		 * 以及发生的任何错误。
		 */
		fc_vacrel->offnum = fc_offnum;
		fc_itemid = PageGetItemId(fc_page, fc_offnum);

		if (!ItemIdIsUsed(fc_itemid))
			continue;

		/* 重定向项不得被触摸 */
		if (ItemIdIsRedirected(fc_itemid))
		{
			fc_prunestate->hastup = true;	/* 页面将无法被截断 */
			continue;
		}

		/*
		 * LP_DEAD 项在循环外处理。
		 *
		 * 请注意，在这里我们故意不将 hastup=true 设置为 LP_DEAD 项，这与 count_nondeletable_pages() 的处理方式不同——该函数仅在页面没有任何项时（除了 LP_UNUSED 项）才认为页面是空的/可截断的。
		 *
		 * 我们的假设是我们在这里遇到的任何 LP_DEAD 项将在实际调用 count_nondeletable_pages() 之前，在 lazy_vacuum_heap_page() 内部变为 LP_UNUSED。无论如何，我们对页面是否 'hastup' 的看法（这就是我们的调用者如何设置其 vacrel->nonempty_pages 值）本质上是竞争敏感的。它必须被视为建议性的/不可靠的，因此我们可以稍微乐观一些。
		 */
		if (ItemIdIsDead(fc_itemid))
		{
			fc_deadoffsets[fc_lpdead_items++] = fc_offnum;
			fc_prunestate->all_visible = false;
			fc_prunestate->has_lpdead_items = true;
			continue;
		}

		Assert(ItemIdIsNormal(fc_itemid));

		ItemPointerSet(&(fc_tuple.t_self), fc_blkno, fc_offnum);
		fc_tuple.t_data = (HeapTupleHeader) PageGetItem(fc_page, fc_itemid);
		fc_tuple.t_len = ItemIdGetLength(fc_itemid);
		fc_tuple.t_tableOid = RelationGetRelid(fc_rel);

		/*
		 * DEAD 元组几乎总是由 heap_page_prune() 修剪为 LP_DEAD 行指针，但可能在 heap_page_prune() 查找后，元组状态已经改变。在这里通过重新开始处理。
		 * （请查看函数顶部的注释以获得完整解释。）
		 */
		fc_res = HeapTupleSatisfiesVacuum(&fc_tuple, fc_vacrel->OldestXmin, fc_buf);

		if (unlikely(fc_res == HEAPTUPLE_DEAD))
			goto retry;

		/*
		 * 该块中将元组计为活动的标准需要与 analyze.c 的 acquire_sample_rows() 的行为匹配，否则 VACUUM 和 ANALYZE 可能会产生 wildly 不同的 reltuples 值，例如当有许多最近死亡的元组时。
		 *
		 * 这里的逻辑比 acquire_sample_rows() 简单，因 VACUUM 不能在事务块中运行，这使得某些情况变为不可能（例如，来自同一事务的进行中的插入）。
		 *
		 * 我们也对 LP_DEAD 项（在这里可能看到的最接近 DEAD 元组的东西）采取不同的处理：我们假设它们将在 VACUUM 完成之前变为 LP_UNUSED。这种差异只是表面的。最终，VACUUM 与 ANALYZE 对于 DEAD 项是有效一致的。VACUUM 不会记住 LP_DEAD 项，但仅仅因为在其完成时不应该留下它们。
		 * （绕过索引清理的情况将违反这一乐观假设，但整体影响应该是微不足道的。）
		 */
		switch (fc_res)
		{
			case HEAPTUPLE_LIVE:

				/*
				 * 计为活动状态。这不仅是自然的，而且也是 acquire_sample_rows() 的行为。
				 */
				fc_live_tuples++;

				/*
				 * 元组对所有事务绝对可见吗？
				 *
				 * 注意：与逐元组提示位一样，如果插入者异步提交，我们无法设置 PD_ALL_VISIBLE 标志。有关更多信息，请参见 SetHintBits。检查该元组的提示 xmin 提交情况。
				 */
				if (fc_prunestate->all_visible)
				{
					TransactionId fc_xmin;

					if (!HeapTupleHeaderXminCommitted(fc_tuple.t_data))
					{
						fc_prunestate->all_visible = false;
						break;
					}

					/*
					 * 插入者肯定已提交。但它是否足够旧，以至于每个人都将其视为已提交？
					 */
					fc_xmin = HeapTupleHeaderGetXmin(fc_tuple.t_data);
					if (!TransactionIdPrecedes(fc_xmin, fc_vacrel->OldestXmin))
					{
						fc_prunestate->all_visible = false;
						break;
					}

					/* 跟踪页面上的最新 xmin。 */
					if (TransactionIdFollows(fc_xmin, fc_prunestate->visibility_cutoff_xid))
						fc_prunestate->visibility_cutoff_xid = fc_xmin;
				}
				break;
			case HEAPTUPLE_RECENTLY_DEAD:

				/*
				 * 如果元组最近已死亡，则必须不将其从关系中删除。（我们只删除从修剪中得到的 LP_DEAD 项。）
				 */
				fc_recently_dead_tuples++;
				fc_prunestate->all_visible = false;
				break;
			case HEAPTUPLE_INSERT_IN_PROGRESS:

				/*
				 * 我们不将这些行视为活动行，因为我们预计插入事务将在提交时更新计数器，我们假设只有在我们报告结果之后才会发生这一点。这个假设有点 shaky，但这正是 acquire_sample_rows() 的做法，因此要保持一致。
				 */
				fc_prunestate->all_visible = false;
				break;
			case HEAPTUPLE_DELETE_IN_PROGRESS:
				/* 这是并发清理期间的一个预期情况 */
				fc_prunestate->all_visible = false;

				/*
				 * 将此类行计为活动行。如上所述，我们假设删除事务将在我们报告后提交并更新计数器。
				 */
				fc_live_tuples++;
				break;
			default:
				elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
				break;
		}

		/*
		 * 不可删除的元组（即具有存储的元组）。
		 *
		 * 检查修剪后留下的元组，看它是否现在需要冻结。
		 */
		fc_prunestate->hastup = true;	/* 页面使得关系截断不安全 */
		if (heap_prepare_freeze_tuple(fc_tuple.t_data,
									  fc_vacrel->relfrozenxid,
									  fc_vacrel->relminmxid,
									  fc_vacrel->FreezeLimit,
									  fc_vacrel->MultiXactCutoff,
									  &fc_frozen[fc_nfrozen], &fc_tuple_totally_frozen,
									  &fc_NewRelfrozenXid, &fc_NewRelminMxid))
		{
			/* 将在下面执行冻结 */
			fc_frozen[fc_nfrozen++].offset = fc_offnum;
		}

		/*
		 * 如果元组没有被冻结（并且也不会变成冻结），那么调用者最好不要继续设置该页面的 VM 位。
		 */
		if (!fc_tuple_totally_frozen)
			fc_prunestate->all_frozen = false;
	}

	fc_vacrel->offnum = InvalidOffsetNumber;

	/*
	 * 我们现在已经将页面上的每个项目分为要在索引中稍后清理的 LP_DEAD 项目或需要考虑现在冻结的 LP_NORMAL 元组（LP_UNUSED 和 LP_REDIRECT 项目也保留，但对我们没有进一步的兴趣）。
	 */
	fc_vacrel->NewRelfrozenXid = fc_NewRelfrozenXid;
	fc_vacrel->NewRelminMxid = fc_NewRelminMxid;

	/*
	 * 首先考虑冻结页面中任何具有元组存储的项目的需要（任意）。
	 */
	if (fc_nfrozen > 0)
	{
		Assert(fc_prunestate->hastup);

		/*
		 * 至少有一个带存储的元组需要被冻结——现在执行冻结。
		 *
		 * 如果我们需要冻结任何元组，我们将标记缓冲区为脏，并写入一个 WAL 记录以记录更改。我们必须记录更改以便在未来 CLOG 截断时保持崩溃安全。
		 */
		START_CRIT_SECTION();

		MarkBufferDirty(fc_buf);

		/* 执行已收集的冻结 */
		for (int fc_i = 0; fc_i < fc_nfrozen; fc_i++)
		{
			HeapTupleHeader fc_htup;

			fc_itemid = PageGetItemId(fc_page, fc_frozen[fc_i].offset);
			fc_htup = (HeapTupleHeader) PageGetItem(fc_page, fc_itemid);

			heap_execute_freeze_tuple(fc_htup, &fc_frozen[fc_i]);
		}

		/* 现在如有必要，进行 WAL 日志记录冻结 */
		if (RelationNeedsWAL(fc_vacrel->rel))
		{
			XLogRecPtr	fc_recptr;

			fc_recptr = log_heap_freeze(fc_vacrel->rel, fc_buf, fc_vacrel->FreezeLimit,
									 fc_frozen, fc_nfrozen);
			PageSetLSN(fc_page, fc_recptr);
		}

		END_CRIT_SECTION();
	}

	/*
	 * 对堆的第二次遍历还可以设置可见性映射位，使用相同的方法。当表在我们到达时每页上通常有几个旧的 LP_DEAD 项目时，这一点很重要（通常是因为过去的机会修剪操作释放了一些非 HOT 元组）。
	 *
	 * VACUUM 将在第二次遍历堆时调用 heap_page_is_all_visible() 来确定页面的 all_visible 和 all_frozen——这是该函数逻辑的一个专用版本。现在我们已经完成了修剪和冻结，确保我们与 heap_page_is_all_visible() 完全一致，使用断言。
	 */
#ifdef USE_ASSERT_CHECKING
	/* 请注意，当 !all_visible 时，all_frozen 值无关紧要 */
	if (fc_prunestate->all_visible)
	{
		TransactionId fc_cutoff;
		bool		fc_all_frozen;

		if (!fc_heap_page_is_all_visible(fc_vacrel, fc_buf, &fc_cutoff, &fc_all_frozen))
			Assert(false);

		Assert(fc_lpdead_items == 0);
		Assert(fc_prunestate->all_frozen == fc_all_frozen);

		/*
		 * 我们冻结了元组并使页面的 XID 截止（出于恢复冲突的目的）为 FrozenTransactionId。这是可以的，因为 visibility_cutoff_xid 会在我们的调用者稍后记录。
		 */
		Assert(fc_cutoff == FrozenTransactionId ||
			   fc_cutoff == fc_prunestate->visibility_cutoff_xid);
	}
#endif

	/*
	 * 现在从页面中保存 LP_DEAD 项目的详细信息到 vacrel。
	 */
	if (fc_lpdead_items > 0)
	{
		VacDeadItems *fc_dead_items = fc_vacrel->dead_items;
		ItemPointerData fc_tmp;

		Assert(!fc_prunestate->all_visible);
		Assert(fc_prunestate->has_lpdead_items);

		fc_vacrel->lpdead_item_pages++;

		ItemPointerSetBlockNumber(&fc_tmp, fc_blkno);

		for (int fc_i = 0; fc_i < fc_lpdead_items; fc_i++)
		{
			ItemPointerSetOffsetNumber(&fc_tmp, fc_deadoffsets[fc_i]);
			fc_dead_items->items[fc_dead_items->num_items++] = fc_tmp;
		}

		Assert(fc_dead_items->num_items <= fc_dead_items->max_items);
		pgstat_progress_update_param(PROGRESS_VACUUM_NUM_DEAD_TUPLES,
									 fc_dead_items->num_items);
	}

	/* 最后，将页面本地计数添加到整个 VACUUM 计数中 */
	fc_vacrel->tuples_deleted += fc_tuples_deleted;
	fc_vacrel->lpdead_items += fc_lpdead_items;
	fc_vacrel->live_tuples += fc_live_tuples;
	fc_vacrel->recently_dead_tuples += fc_recently_dead_tuples;
}

/*
 *	lazy_scan_noprune() -- lazy_scan_prune() 无修剪或冻结
 *
 * 调用者只需在缓冲区上持有一个固定锁和共享锁，与
 * lazy_scan_prune 相比，这里不需要完全的清理锁。虽然这里不执行修剪，
 * 但很可能早期的机会性修剪操作留下了 LP_DEAD 项。我们至少会将任何此类项目
 * 收集到 dead_items 数组中以供从索引中删除。
 *
 * 对于激进的 VACUUM 调用者，我们可能返回 false 以指示需要
 * 完整的清理锁来由 lazy_scan_prune 处理。这仅在激进的 VACUUM 需要从
 * 页面上的一个或多个元组中冻结一些元组 XID 时才是必要的。
 * 对于非激进的调用者，我们始终返回 true。
 *
 * 请参见 lazy_scan_prune 以了解 hastup 返回标志的解释。
 * recordfreespace 标志指示调用者是否应对页面执行通用 FSM 处理。
 */
static bool fc_lazy_scan_noprune(LVRelState *fc_vacrel,
				  Buffer fc_buf,
				  BlockNumber fc_blkno,
				  Page fc_page,
				  bool *fc_hastup,
				  bool *fc_recordfreespace)
{
	OffsetNumber fc_offnum,
				fc_maxoff;
	int			fc_lpdead_items,
				fc_live_tuples,
				fc_recently_dead_tuples,
				fc_missed_dead_tuples;
	HeapTupleHeader fc_tupleheader;
	TransactionId fc_NewRelfrozenXid = fc_vacrel->NewRelfrozenXid;
	MultiXactId fc_NewRelminMxid = fc_vacrel->NewRelminMxid;
	OffsetNumber fc_deadoffsets[MaxHeapTuplesPerPage];

	Assert(BufferGetBlockNumber(fc_buf) == fc_blkno);

	*fc_hastup = false;			/* 目前为止 */
	*fc_recordfreespace = false;	/* 目前为止 */

	fc_lpdead_items = 0;
	fc_live_tuples = 0;
	fc_recently_dead_tuples = 0;
	fc_missed_dead_tuples = 0;

	fc_maxoff = PageGetMaxOffsetNumber(fc_page);
	for (fc_offnum = FirstOffsetNumber;
		 fc_offnum <= fc_maxoff;
		 fc_offnum = OffsetNumberNext(fc_offnum))
	{
		ItemId		fc_itemid;
		HeapTupleData fc_tuple;

		fc_vacrel->offnum = fc_offnum;
		fc_itemid = PageGetItemId(fc_page, fc_offnum);

		if (!ItemIdIsUsed(fc_itemid))
			continue;

		if (ItemIdIsRedirected(fc_itemid))
		{
			*fc_hastup = true;
			continue;
		}

		if (ItemIdIsDead(fc_itemid))
		{
			/*
			 *故意不在这里设置 hastup=true。请参见
			 * lazy_scan_prune 中的相同点以获取解释。
			 */
			fc_deadoffsets[fc_lpdead_items++] = fc_offnum;
			continue;
		}

		*fc_hastup = true;			/* 页面防止关系截断 */
		fc_tupleheader = (HeapTupleHeader) PageGetItem(fc_page, fc_itemid);
		if (heap_tuple_would_freeze(fc_tupleheader,
									fc_vacrel->FreezeLimit,
									fc_vacrel->MultiXactCutoff,
									&fc_NewRelfrozenXid, &fc_NewRelminMxid))
		{
			/* XID < FreezeLimit 的元组（或 MXID < MultiXactCutoff） */
			if (fc_vacrel->aggressive)
			{
				/*
				 * 激进的 VACUUM 必须始终能够将关系的
				 * relfrozenxid 提升到 >= FreezeLimit 的值 
				 * （并能够将关系的 relminmxid 提升到 >= MultiXactCutoff）。
				 * 正在进行的激进 VACUUM 如果不能现在冻结此元组的
				 * XID（或 MXID），就无法做到这一点。
				 *
				 * 唯一安全的选择是让调用者使用 lazy_scan_prune 来处理
				 * 此页面。调用者可能必须等一段时间以获取清理锁，但这无可奈何。
				 */
				fc_vacrel->offnum = InvalidOffsetNumber;
				return false;
			}

			/*
			 * 非激进的 VACUUM 没有义务提升
			 * relfrozenxid（即使是一个 XID）。我们在这里可以松弛得多。
			 *
			 * 目前我们总是接受较旧的 final relfrozenxid
			 * 和/或 relminmxid 值。我们从不让调用者等待或稍微努力，
			 * 即使这可能是合理的。
			 */
		}

		ItemPointerSet(&(fc_tuple.t_self), fc_blkno, fc_offnum);
		fc_tuple.t_data = (HeapTupleHeader) PageGetItem(fc_page, fc_itemid);
		fc_tuple.t_len = ItemIdGetLength(fc_itemid);
		fc_tuple.t_tableOid = RelationGetRelid(fc_vacrel->rel);

		switch (HeapTupleSatisfiesVacuum(&fc_tuple, fc_vacrel->OldestXmin, fc_buf))
		{
			case HEAPTUPLE_DELETE_IN_PROGRESS:
			case HEAPTUPLE_LIVE:

				/*
				 * 将这两种情况都视为活跃的，就像 lazy_scan_prune 一样
				 */
				fc_live_tuples++;

				break;
			case HEAPTUPLE_DEAD:

				/*
				 * 由于未能获得清理锁，有一些有用的修剪工作将不会完成。
				 */
				fc_missed_dead_tuples++;
				break;
			case HEAPTUPLE_RECENTLY_DEAD:

				/*
				 * 将其计入 recently_dead_tuples，就像 lazy_scan_prune 一样
				 */
				fc_recently_dead_tuples++;
				break;
			case HEAPTUPLE_INSERT_IN_PROGRESS:

				/*
				 * 不要将这些行计为活跃，就像 lazy_scan_prune 一样
				 */
				break;
			default:
				elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
				break;
		}
	}

	fc_vacrel->offnum = InvalidOffsetNumber;

	/*
	 * 到这里我们可以确定调用者可以将冻结和修剪
	 * 此特定页面推迟到下一个 VACUUM。现在记住其细节。
	 * （lazy_scan_prune 期望一个干净的状态，因此我们必须最后执行此操作。）
	 */
	fc_vacrel->NewRelfrozenXid = fc_NewRelfrozenXid;
	fc_vacrel->NewRelminMxid = fc_NewRelminMxid;

	/* 将在页面上找到的任何 LP_DEAD 项保存在 dead_items 数组中 */
	if (fc_vacrel->nindexes == 0)
	{
		/* 使用单遍策略（因为表没有索引） */
		if (fc_lpdead_items > 0)
		{
			/*
			 * 对于一个单一的 VACUUM 策略无法获取清理锁的边缘情况的例行处理，并且发现存在一个或多个 LP_DEAD 项：将 LP_DEAD 项计数为 missed_dead_tuples。 （这有点不诚实，但总比永远维护专门的堆清理代码要好，收益微乎其微。）
			 */
			*fc_hastup = true;
			fc_missed_dead_tuples += fc_lpdead_items;
		}

		*fc_recordfreespace = true;
	}
	else if (fc_lpdead_items == 0)
	{
		/*
		 * 不会在稍后清理此页面，因此现在记录页面的自由空间到 FSM
		 */
		*fc_recordfreespace = true;
	}
	else
	{
		VacDeadItems *fc_dead_items = fc_vacrel->dead_items;
		ItemPointerData fc_tmp;

		/*
		 * 页面有 LP_DEAD 项，因此在索引清理期间，索引中保留的任何引用/TID 将被删除（然后在堆中标记为 LP_UNUSED）
		 */
		fc_vacrel->lpdead_item_pages++;

		ItemPointerSetBlockNumber(&fc_tmp, fc_blkno);

		for (int fc_i = 0; fc_i < fc_lpdead_items; fc_i++)
		{
			ItemPointerSetOffsetNumber(&fc_tmp, fc_deadoffsets[fc_i]);
			fc_dead_items->items[fc_dead_items->num_items++] = fc_tmp;
		}

		Assert(fc_dead_items->num_items <= fc_dead_items->max_items);
		pgstat_progress_update_param(PROGRESS_VACUUM_NUM_DEAD_TUPLES,
									 fc_dead_items->num_items);

		fc_vacrel->lpdead_items += fc_lpdead_items;

		/*
		 * 假设我们将在最后一遍遍历堆时清理此堆页面。 在那之前不要记录自由空间。
		 */
		*fc_recordfreespace = false;
	}

	/*
	 * 最后，将相关的页面本地计数添加到整个 VACUUM 计数中
	 */
	fc_vacrel->live_tuples += fc_live_tuples;
	fc_vacrel->recently_dead_tuples += fc_recently_dead_tuples;
	fc_vacrel->missed_dead_tuples += fc_missed_dead_tuples;
	if (fc_missed_dead_tuples > 0)
		fc_vacrel->missed_dead_pages++;

	/* 调用者不需要在同一页面上调用 lazy_scan_prune */
	return true;
}

/*
 * 索引清理和堆清理的主要入口点。
 *
 * 从表的索引中删除在 dead_items 中收集的项目，然后在堆中将相同的项目标记为 LP_UNUSED。有关详细信息，请参见 lazy_scan_heap 上方的注释。
 *
 * 还会清空 dead_items，为后来的 TID 腾出空间。
 *
 * 我们可以选择在此时绕过索引清理，尽管只有在正在进行的 VACUUM 操作肯定只有一个索引扫描/一轮索引清理时才能这样做。
 */
static void fc_lazy_vacuum(LVRelState *fc_vacrel)
{
	bool		fc_bypass;

	/* 不应在没有索引的情况下到达此处 */
	Assert(fc_vacrel->nindexes > 0);
	Assert(fc_vacrel->lpdead_item_pages > 0);

	if (!fc_vacrel->do_index_vacuuming)
	{
		Assert(!fc_vacrel->do_index_cleanup);
		fc_vacrel->dead_items->num_items = 0;
		return;
	}

	/*
	 * 考虑完全绕过索引清理（和堆清理）。
	 *
	 * 我们目前只在整个 VACUUM 操作中 LP_DEAD 项的数量接近于零的情况下这样做。 这避免了动态变化的 VACUUM 操作持续时间和开销的尖锐不连续性，这些操作针对同一表并具有固定工作量。
	 * 理想情况下，当 LP_DEAD 项接近零时，后续 VACUUM 操作将表现得好像有正好零个 LP_DEAD 项。
	 *
	 * 这对于一个持续受到可以大部分应用 HOT 优化的 UPDATE 影响的表可能会有帮助，但偶尔会出现一些小的异常，导致只有少量堆页面保留一两个 LP_DEAD 项。 这是非常常见的； 即使 DBA 努力使 UPDATE 使用 HOT，预测 HOT 是否会在 100% 的情况下应用几乎是不可能的。 通过精心调优，确保 99% 以上的所有 UPDATE 针对一个表使用 HOT 要容易得多。
	 */
	fc_bypass = false;
	if (fc_vacrel->consider_bypass_optimization && fc_vacrel->rel_pages > 0)
	{
		BlockNumber fc_threshold;

		Assert(fc_vacrel->num_index_scans == 0);
		Assert(fc_vacrel->lpdead_items == fc_vacrel->dead_items->num_items);
		Assert(fc_vacrel->do_index_vacuuming);
		Assert(fc_vacrel->do_index_cleanup);

		/*
		 * 这个交叉点将在我们开始进行索引清理时，以表中已知至少有一个 LP_DEAD 项的堆页面总数的百分比来表示。这
		 * 比 LP_DEAD 项的总数要重要得多，因为它是堆页面的可见性映射位由于绕过索引和堆清理而无法设置的一个代理。
		 *
		 * 我们还采取一个进一步的预防性测试：当前用于存储 TIDs（现在全部指向 LP_DEAD 项的 TIDs）的空间不得超过
		 * 32MB。这限制了我们一次又一次地绕过索引清理的风险，直到最终出现一个 VACUUM，其 dead_items 空间不在 CPU
		 * 缓存中驻留。
		 *
		 * 在应用优化时，我们不会采取任何特殊步骤来记住 LP_DEAD 项（例如，在我们对统计系统的最终更新中对它们进行收
		 * 计）。虽然 analyze.c 的 acquire_sample_rows() 中的会计会将相同的 LP_DEAD 项识别为其自身统计报告中的死行，
		 * 这也是可以的。差异应该是微不足道的。如果这个优化有朝一日扩展到涵盖更多情况，则可能需要重新考虑这一点。
		 */
		fc_threshold = (double) fc_vacrel->rel_pages * BYPASS_THRESHOLD_PAGES;
		fc_bypass = (fc_vacrel->lpdead_item_pages < fc_threshold &&
				  fc_vacrel->lpdead_items < MAXDEADITEMS(32L * 1024L * 1024L));
	}

	if (fc_bypass)
	{
		/*
		 * TIDs 几乎为零。表现得好像确实是零：绕过索引清理，但进行索引清理。
		 *
		 * 我们预计正在进行的 VACUUM 操作将快速完成，因此没有必要考虑加速作为防止溢出失败的一个安全措施。
		 * （在没有 ambulkdelete() 调用的情况下，索引清理预计将非常快速地完成。）
		 */
		fc_vacrel->do_index_vacuuming = false;
	}
	else if (fc_lazy_vacuum_all_indexes(fc_vacrel))
	{
		/*
		 * 我们成功完成了一轮索引清理。现在进行相关的堆清理。
		 */
		fc_lazy_vacuum_heap_rel(fc_vacrel);
	}
	else
	{
		/*
		 * 安全措施案例。
		 *
		 * 我们尝试了索引清理，但没有完成一轮完整的索引扫描。这发生在 relfrozenxid 或 relminmxid 过于久远的情况。
		 *
		 * 从这一点开始，VACUUM 操作将不再进行进一步的索引清理或堆清理。这个 VACUUM 操作将不会再次回到这里。
		 */
		Assert(fc_vacrel->failsafe_active);
	}

	/*
	 * 忘记我们刚刚清理（或刚决定不清理）的 LP_DEAD 项
	 */
	fc_vacrel->dead_items->num_items = 0;
}

/*
 *	lazy_vacuum_all_indexes() -- 索引清理的主入口
 *
 * 在所有索引成功清理的常见情况下返回 true。在我们确定正在进行的 VACUUM 操作有可能花费过多时间完成，
 * 从而导致溢出失败的罕见情况下返回 false。
 */
static bool fc_lazy_vacuum_all_indexes(LVRelState *fc_vacrel)
{
	bool		fc_allindexes = true;

	Assert(fc_vacrel->nindexes > 0);
	Assert(fc_vacrel->do_index_vacuuming);
	Assert(fc_vacrel->do_index_cleanup);

	/* 为 XID 溢出紧急情况进行预检查 */
	if (fc_lazy_check_wraparound_failsafe(fc_vacrel))
	{
		/* 溢出紧急情况 -- 甚至不要开始索引扫描 */
		return false;
	}

	/* 报告我们正在清理索引 */
	pgstat_progress_update_param(PROGRESS_VACUUM_PHASE,
								 PROGRESS_VACUUM_PHASE_VACUUM_INDEX);

	if (!ParallelVacuumIsActive(fc_vacrel))
	{
		for (int fc_idx = 0; fc_idx < fc_vacrel->nindexes; fc_idx++)
		{
			Relation	fc_indrel = fc_vacrel->indrels[fc_idx];
			IndexBulkDeleteResult *fc_istat = fc_vacrel->indstats[fc_idx];

			fc_vacrel->indstats[fc_idx] =
				fc_lazy_vacuum_one_index(fc_indrel, fc_istat, fc_vacrel->old_live_tuples,
									  fc_vacrel);

			if (fc_lazy_check_wraparound_failsafe(fc_vacrel))
			{
				/* 溢出紧急情况 -- 结束当前的索引扫描 */
				fc_allindexes = false;
				break;
			}
		}
	}
	else
	{
		/* 将所有工作外包给并行变种 */
		parallel_vacuum_bulkdel_all_indexes(fc_vacrel->pvs, fc_vacrel->old_live_tuples,
											fc_vacrel->num_index_scans);

		/*
		 * 进行后检查以考虑现在应用环绕故障安全。注意
		 * 并行 VACUUM 仅进行预检查和此后检查。
		 */
		if (fc_lazy_check_wraparound_failsafe(fc_vacrel))
			fc_allindexes = false;
	}

	/*
	 * 我们在每次调用这里时从所有索引中的第一次堆遍历中删除所有 LP_DEAD 项
	 * （除了我们选择执行故障安全的调用）。这使得下一次调用 lazy_vacuum_heap_rel() 是安全的
	 * （除了在故障安全触发的情况下，这将阻止下一次调用的发生）。
	 */
	Assert(fc_vacrel->num_index_scans > 0 ||
		   fc_vacrel->dead_items->num_items == fc_vacrel->lpdead_items);
	Assert(fc_allindexes || fc_vacrel->failsafe_active);

	/*
	 * 增加并报告索引扫描的数量。
	 *
	 * 我们故意包括未能完成的批量删除的情况，因为故障安全触发了。
	 */
	fc_vacrel->num_index_scans++;
	pgstat_progress_update_param(PROGRESS_VACUUM_NUM_INDEX_VACUUMS,
								 fc_vacrel->num_index_scans);

	return fc_allindexes;
}

/*
 *	lazy_vacuum_heap_rel() -- 采用两次遍历策略对堆进行第二次遍历
 *
 * 此例程将 vacrel->dead_items 数组中的 LP_DEAD 项标记为 LP_UNUSED。
 * 从未进行 lazy_scan_prune 的页面不会被访问。
 *
 * 我们也可能能够修剪我们访问的堆页面的行指针数组。如果数组末尾有一组连续的 
 * LP_UNUSED 项，可以作为空闲空间回收。这些 LP_UNUSED 项通常最初是由 
 * lazy_scan_prune 记录的 LP_DEAD 项（我们将每个页面的项设置为 LP_UNUSED， 
 * 然后考虑是否可能修剪页面的行指针数组）。
 *
 * 注意：之所以进行第二次遍历，是因为在删除元组之前无法删除它们的索引条目，
 * 我们希望尽可能大批量地处理索引条目的移除。
 */
static void fc_lazy_vacuum_heap_rel(LVRelState *fc_vacrel)
{
	int			fc_index;
	BlockNumber fc_vacuumed_pages;
	Buffer		fc_vmbuffer = InvalidBuffer;
	LVSavedErrInfo fc_saved_err_info;

	Assert(fc_vacrel->do_index_vacuuming);
	Assert(fc_vacrel->do_index_cleanup);
	Assert(fc_vacrel->num_index_scans > 0);

	/* 报告我们现在正在清理堆 */
	pgstat_progress_update_param(PROGRESS_VACUUM_PHASE,
								 PROGRESS_VACUUM_PHASE_VACUUM_HEAP);

	/* 更新错误追踪信息 */
	fc_update_vacuum_error_info(fc_vacrel, &fc_saved_err_info,
							 VACUUM_ERRCB_PHASE_VACUUM_HEAP,
							 InvalidBlockNumber, InvalidOffsetNumber);

	fc_vacuumed_pages = 0;

	fc_index = 0;
	while (fc_index < fc_vacrel->dead_items->num_items)
	{
		BlockNumber fc_tblk;
		Buffer		fc_buf;
		Page		fc_page;
		Size		fc_freespace;

		vacuum_delay_point();

		fc_tblk = ItemPointerGetBlockNumber(&fc_vacrel->dead_items->items[fc_index]);
		fc_vacrel->blkno = fc_tblk;
		fc_buf = ReadBufferExtended(fc_vacrel->rel, MAIN_FORKNUM, fc_tblk, RBM_NORMAL,
								 fc_vacrel->bstrategy);
		LockBuffer(fc_buf, BUFFER_LOCK_EXCLUSIVE);
		fc_index = fc_lazy_vacuum_heap_page(fc_vacrel, fc_tblk, fc_buf, fc_index, &fc_vmbuffer);

		/* 现在我们已经清理了页面，记录其可用空间 */
		fc_page = BufferGetPage(fc_buf);
		fc_freespace = PageGetHeapFreeSpace(fc_page);

		UnlockReleaseBuffer(fc_buf);
		RecordPageWithFreeSpace(fc_vacrel->rel, fc_tblk, fc_freespace);
		fc_vacuumed_pages++;
	}

	/* 清除块编号信息 */
	fc_vacrel->blkno = InvalidBlockNumber;

	if (BufferIsValid(fc_vmbuffer))
	{
		ReleaseBuffer(fc_vmbuffer);
		fc_vmbuffer = InvalidBuffer;
	}

	/*
	 * 在第二次堆遍历中，我们将第一次堆遍历中的所有 LP_DEAD 项设置为 LP_UNUSED。
	 * 没有更多，也没有更少。
	 */
	Assert(fc_index > 0);
	Assert(fc_vacrel->num_index_scans > 1 ||
		   (fc_index == fc_vacrel->lpdead_items &&
			fc_vacuumed_pages == fc_vacrel->lpdead_item_pages));

	ereport(DEBUG2,
			(errmsg("table \"%s\": removed %lld dead item identifiers in %u pages",
					fc_vacrel->relname, (long long) fc_index, fc_vacuumed_pages)));

	/* 恢复之前的阶段信息以便错误追踪 */
	fc_restore_vacuum_error_info(fc_vacrel, &fc_saved_err_info);
}

/*
 *	lazy_vacuum_heap_page() -- 释放页中列出的 LP_DEAD 项
 *						  vacrel->dead_items 数组。
 *
 * 调用者必须对缓冲区具有独占的缓冲区锁（虽然完全清理锁也是可以的）。
 *
 * index 是 vacrel->dead_items 数组中页面上第一个列出的 LP_DEAD 项的偏移。 
 * 返回值是数组中同一页面所有 LP_DEAD 项之后的第一个索引。
 */
static int fc_lazy_vacuum_heap_page(LVRelState *fc_vacrel, BlockNumber fc_blkno, Buffer fc_buffer,
					  int fc_index, Buffer *fc_vmbuffer)
{
	VacDeadItems *fc_dead_items = fc_vacrel->dead_items;
	Page		fc_page = BufferGetPage(fc_buffer);
	OffsetNumber fc_unused[MaxHeapTuplesPerPage];
	int			fc_uncnt = 0;
	TransactionId fc_visibility_cutoff_xid;
	bool		fc_all_frozen;
	LVSavedErrInfo fc_saved_err_info;

	Assert(fc_vacrel->nindexes == 0 || fc_vacrel->do_index_vacuuming);

	pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_VACUUMED, fc_blkno);

	/* 更新错误追踪信息 */
	fc_update_vacuum_error_info(fc_vacrel, &fc_saved_err_info,
							 VACUUM_ERRCB_PHASE_VACUUM_HEAP, fc_blkno,
							 InvalidOffsetNumber);

	START_CRIT_SECTION();

	for (; fc_index < fc_dead_items->num_items; fc_index++)
	{
		BlockNumber fc_tblk;
		OffsetNumber fc_toff;
		ItemId		fc_itemid;

		fc_tblk = ItemPointerGetBlockNumber(&fc_dead_items->items[fc_index]);
		if (fc_tblk != fc_blkno)
			break;				/* 超出该块的元组结束 */
		fc_toff = ItemPointerGetOffsetNumber(&fc_dead_items->items[fc_index]);
		fc_itemid = PageGetItemId(fc_page, fc_toff);

		Assert(ItemIdIsDead(fc_itemid) && !ItemIdHasStorage(fc_itemid));
		ItemIdSetUnused(fc_itemid);
		fc_unused[fc_uncnt++] = fc_toff;
	}

	Assert(fc_uncnt > 0);

	/* 现在尝试截断行指针数组 */
	PageTruncateLinePointerArray(fc_page);

	/*
	 * 在我们写入 WAL 之前标记缓冲区为脏。
	 */
	MarkBufferDirty(fc_buffer);

	/* XLOG 相关内容 */
	if (RelationNeedsWAL(fc_vacrel->rel))
	{
		xl_heap_vacuum fc_xlrec;
		XLogRecPtr	fc_recptr;

		fc_xlrec.nunused = fc_uncnt;

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, SizeOfHeapVacuum);

		XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD);
		XLogRegisterBufData(0, (char *) fc_unused, fc_uncnt * sizeof(OffsetNumber));

		fc_recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_VACUUM);

		PageSetLSN(fc_page, fc_recptr);
	}

	/*
	 * 结束关键区段，这样我们可以安全地进行可见性测试（这 
	 * 可能需要执行IO并分配内存！）。如果我们现在崩溃，页面
	 * （包括相应的 vm 位）可能不会标记为全可见，但这没关系。
	 * 稍后的清理将修复这一点。
	 */
	END_CRIT_SECTION();

	/*
	 * 现在我们已经从页面中移除了 LD_DEAD 项，再次检查页面是否变为全可见。页面已经标记为脏，独占锁定，并且如果需要，已发出完整的页面图像。
	 */
	if (fc_heap_page_is_all_visible(fc_vacrel, fc_buffer, &fc_visibility_cutoff_xid,
								 &fc_all_frozen))
		PageSetAllVisible(fc_page);

	/*
	 * 对堆页面的所有更改已完成。如果现在设置了全可见标志，还要设置 VM 全可见位（如果可能，还要设置全冻结位），除非之前已经完成此操作。
	 */
	if (PageIsAllVisible(fc_page))
	{
		uint8		fc_flags = 0;
		uint8		fc_vm_status = visibilitymap_get_status(fc_vacrel->rel,
														 fc_blkno, fc_vmbuffer);

		/* 如果需要，设置 VM 全冻结位 */
		if ((fc_vm_status & VISIBILITYMAP_ALL_VISIBLE) == 0)
			fc_flags |= VISIBILITYMAP_ALL_VISIBLE;
		if ((fc_vm_status & VISIBILITYMAP_ALL_FROZEN) == 0 && fc_all_frozen)
			fc_flags |= VISIBILITYMAP_ALL_FROZEN;

		Assert(BufferIsValid(*fc_vmbuffer));
		if (fc_flags != 0)
			visibilitymap_set(fc_vacrel->rel, fc_blkno, fc_buffer, InvalidXLogRecPtr,
							  *fc_vmbuffer, fc_visibility_cutoff_xid, fc_flags);
	}

	/* 恢复之前的阶段信息以便错误追踪 */
	fc_restore_vacuum_error_info(fc_vacrel, &fc_saved_err_info);
	return fc_index;
}

/*
 * 触发安全失败以避免在 vacrel 表具有 relfrozenxid 和/或 relminmxid 时的回绕失败，这些值在时间上非常久远。
 * 触发安全失败使正在进行的 VACUUM 跳过任何进一步的索引 VACUUM 和堆 VACUUM。截断堆也被绕过。
 *
 * 任何剩余工作（VACUUM 无法跳过的工作）在触发安全失败时通常会加快速度。
 * VACUUM 停止应用它最开始使用的基于成本的延迟。
 *
 * 当触发安全失败时返回 true。
 */
static bool fc_lazy_check_wraparound_failsafe(LVRelState *fc_vacrel)
{
	Assert(TransactionIdIsNormal(fc_vacrel->relfrozenxid));
	Assert(MultiXactIdIsValid(fc_vacrel->relminmxid));

	/* 每次 VACUUM 不要警告超过一次 */
	if (fc_vacrel->failsafe_active)
		return true;

	if (unlikely(vacuum_xid_failsafe_check(fc_vacrel->relfrozenxid,
										   fc_vacrel->relminmxid)))
	{
		fc_vacrel->failsafe_active = true;

		/* 禁用索引 VACUUM、索引清理和堆关系截断 */
		fc_vacrel->do_index_vacuuming = false;
		fc_vacrel->do_index_cleanup = false;
		fc_vacrel->do_rel_truncate = false;

		ereport(WARNING,
				(errmsg("bypassing nonessential maintenance of table \"%s.%s.%s\" as a failsafe after %d index scans",
						get_database_name(MyDatabaseId),
						fc_vacrel->relnamespace,
						fc_vacrel->relname,
						fc_vacrel->num_index_scans),
				 errdetail("The table's relfrozenxid or relminmxid is too far in the past."),
				 errhint("Consider increasing configuration parameter \"maintenance_work_mem\" or \"autovacuum_work_mem\".\n"
						 "You might also need to consider other ways for VACUUM to keep up with the allocation of transaction IDs.")));

		/* 从这一点开始停止应用成本限制 */
		VacuumCostActive = false;
		VacuumCostBalance = 0;

		return true;
	}

	return false;
}

/*
 *	lazy_cleanup_all_indexes() -- 清理关系的所有索引。
 */
static void fc_lazy_cleanup_all_indexes(LVRelState *fc_vacrel)
{
	double		fc_reltuples = fc_vacrel->new_rel_tuples;
	bool		fc_estimated_count = fc_vacrel->scanned_pages < fc_vacrel->rel_pages;

	Assert(fc_vacrel->do_index_cleanup);
	Assert(fc_vacrel->nindexes > 0);

	/* 报告我们现在正在清理索引 */
	pgstat_progress_update_param(PROGRESS_VACUUM_PHASE,
								 PROGRESS_VACUUM_PHASE_INDEX_CLEANUP);

	if (!ParallelVacuumIsActive(fc_vacrel))
	{
		for (int fc_idx = 0; fc_idx < fc_vacrel->nindexes; fc_idx++)
		{
			Relation	fc_indrel = fc_vacrel->indrels[fc_idx];
			IndexBulkDeleteResult *fc_istat = fc_vacrel->indstats[fc_idx];

			fc_vacrel->indstats[fc_idx] =
				fc_lazy_cleanup_one_index(fc_indrel, fc_istat, fc_reltuples,
									   fc_estimated_count, fc_vacrel);
		}
	}
	else
	{
		/* 将所有工作外包给并行变种 */
		parallel_vacuum_cleanup_all_indexes(fc_vacrel->pvs, fc_reltuples,
											fc_vacrel->num_index_scans,
											fc_estimated_count);
	}
}

/*
 *	lazy_vacuum_one_index() -- VACUUM 索引关系。
 *
 *		删除在 vacrel->dead_items 数组中收集的所有包含 TID 的索引元组。还更新运行统计信息。
 *		具体细节依赖于索引 AM 的 ambulkdelete 程序。
 *
 *		reltuples 是传递给批量删除回调的堆元组数。它始终假定为估算值。
 *		有关更多信息，请参见 indexam.sgml。
 *
 * 返回从输入统计中派生的批量删除统计信息
 */
static IndexBulkDeleteResult * fc_lazy_vacuum_one_index(Relation fc_indrel, IndexBulkDeleteResult *fc_istat,
					  double fc_reltuples, LVRelState *fc_vacrel)
{
	IndexVacuumInfo fc_ivinfo;
	LVSavedErrInfo fc_saved_err_info;

	fc_ivinfo.index = fc_indrel;
	fc_ivinfo.analyze_only = false;
	fc_ivinfo.report_progress = false;
	fc_ivinfo.estimated_count = true;
	fc_ivinfo.message_level = DEBUG2;
	fc_ivinfo.num_heap_tuples = fc_reltuples;
	fc_ivinfo.strategy = fc_vacrel->bstrategy;

	/*
	 * 更新错误回溯信息。
	 *
	 * 在此阶段保存索引名称，并在此阶段结束后立即恢复。请参见 vacuum_error_callback。
	 */
	Assert(fc_vacrel->indname == NULL);
	fc_vacrel->indname = pstrdup(RelationGetRelationName(fc_indrel));
	fc_update_vacuum_error_info(fc_vacrel, &fc_saved_err_info,
							 VACUUM_ERRCB_PHASE_VACUUM_INDEX,
							 InvalidBlockNumber, InvalidOffsetNumber);

	/* 执行批量删除 */
	fc_istat = vac_bulkdel_one_index(&fc_ivinfo, fc_istat, (void *) fc_vacrel->dead_items);

	/* 恢复之前的阶段信息以便错误追踪 */
	fc_restore_vacuum_error_info(fc_vacrel, &fc_saved_err_info);
	pfree(fc_vacrel->indname);
	fc_vacrel->indname = NULL;

	return fc_istat;
}

/*
 *	lazy_cleanup_one_index() -- 对索引关系进行 VACUUM 后清理。
 *
 *		调用索引 AM 的 amvacuumcleanup 程序。 reltuples 是堆元组的数量，如果 reltuples 是估计值，则 estimated_count 为真。
 *		有关更多信息，请参见 indexam.sgml。
 *
 * 返回从输入统计中派生的批量删除统计信息
 */
static IndexBulkDeleteResult * fc_lazy_cleanup_one_index(Relation fc_indrel, IndexBulkDeleteResult *fc_istat,
					   double fc_reltuples, bool fc_estimated_count,
					   LVRelState *fc_vacrel)
{
	IndexVacuumInfo fc_ivinfo;
	LVSavedErrInfo fc_saved_err_info;

	fc_ivinfo.index = fc_indrel;
	fc_ivinfo.analyze_only = false;
	fc_ivinfo.report_progress = false;
	fc_ivinfo.estimated_count = fc_estimated_count;
	fc_ivinfo.message_level = DEBUG2;

	fc_ivinfo.num_heap_tuples = fc_reltuples;
	fc_ivinfo.strategy = fc_vacrel->bstrategy;

	/*
	 * 更新错误回溯信息。
	 *
	 * 在此阶段保存索引名称，并在此阶段结束后立即恢复。请参见 vacuum_error_callback。
	 */
	Assert(fc_vacrel->indname == NULL);
	fc_vacrel->indname = pstrdup(RelationGetRelationName(fc_indrel));
	fc_update_vacuum_error_info(fc_vacrel, &fc_saved_err_info,
							 VACUUM_ERRCB_PHASE_INDEX_CLEANUP,
							 InvalidBlockNumber, InvalidOffsetNumber);

	fc_istat = vac_cleanup_one_index(&fc_ivinfo, fc_istat);

	/* 恢复之前的阶段信息以便错误追踪 */
	fc_restore_vacuum_error_info(fc_vacrel, &fc_saved_err_info);
	pfree(fc_vacrel->indname);
	fc_vacrel->indname = NULL;

	return fc_istat;
}

/*
 * should_attempt_truncation - 我们应该尝试截断堆吗？
 *
 * 除非我们有机会释放大量页面，否则不要考虑这个。否则，所花费的时间是不值得的，主要是因为在任何热备份上必须重放一个 AccessExclusive 锁，这可能会特别具破坏性。
 *
 * 如果 wraparound failsafe 生效，也请不要尝试。此时系统可能会拒绝分配新的 XIDs。系统肯定不会恢复正常，除非 VACUUM 实际上推进了最旧的 relfrozenxid——而这对于目标 rel 还没有发生。如果 lazy_truncate_heap 在这种情况下试图获取 AccessExclusiveLock 来截断表，则可能出现 XID 耗尽错误，使得 VACUUM 无法解决潜在的 XID 耗尽问题。在任何情况下，当 failsafe 生效时，截断成功的机会非常小。lazy_scan_prune 做出了乐观假设，即它遇到的任何 LP_DEAD 项在我们被调用时总会是 LP_UNUSED。
 *
 * 如果我们正在进行早期修剪/清理，也请不要尝试，因为无法找到已截断的堆页面的扫描无法确定快照是否太旧而无法读取该页面。
 */
static bool fc_should_attempt_truncation(LVRelState *fc_vacrel)
{
	BlockNumber fc_possibly_freeable;

	if (!fc_vacrel->do_rel_truncate || fc_vacrel->failsafe_active ||
		old_snapshot_threshold >= 0)
		return false;

	fc_possibly_freeable = fc_vacrel->rel_pages - fc_vacrel->nonempty_pages;
	if (fc_possibly_freeable > 0 &&
		(fc_possibly_freeable >= REL_TRUNCATE_MINIMUM ||
		 fc_possibly_freeable >= fc_vacrel->rel_pages / REL_TRUNCATE_FRACTION))
		return true;

	return false;
}

/*
 * lazy_truncate_heap - 尝试截断末尾的任何空页面
 */
static void fc_lazy_truncate_heap(LVRelState *fc_vacrel)
{
	BlockNumber fc_orig_rel_pages = fc_vacrel->rel_pages;
	BlockNumber fc_new_rel_pages;
	bool		fc_lock_waiter_detected;
	int			fc_lock_retry;

	/* 报告我们现在正在截断 */
	pgstat_progress_update_param(PROGRESS_VACUUM_PHASE,
								 PROGRESS_VACUUM_PHASE_TRUNCATE);

	/* 最后一次更新错误追踪信息 */
	fc_update_vacuum_error_info(fc_vacrel, NULL, VACUUM_ERRCB_PHASE_TRUNCATE,
							 fc_vacrel->nonempty_pages, InvalidOffsetNumber);

	/*
	 * 循环直到无法进行更多的截断。
	 */
	do
	{
		/*
		 * 我们需要在关系上获得完全独占锁才能执行截断。如果我们无法获取，则放弃而不是等待——我们不想阻塞其他后端，也不想死锁（考虑到我们已经持有较低级别的锁，这种情况是很可能的）。
		 */
		fc_lock_waiter_detected = false;
		fc_lock_retry = 0;
		while (true)
		{
			if (ConditionalLockRelation(fc_vacrel->rel, AccessExclusiveLock))
				break;

			/*
			 * 在尝试（重新）获取独占锁时检查中断。
			 */
			CHECK_FOR_INTERRUPTS();

			if (++fc_lock_retry > (VACUUM_TRUNCATE_LOCK_TIMEOUT /
								VACUUM_TRUNCATE_LOCK_WAIT_INTERVAL))
			{
				/*
				 * 我们在指定的重试次数内未能建立锁。这意味着我们放弃截断。
				 */
				ereport(fc_vacrel->verbose ? INFO : DEBUG2,
						(errmsg("\"%s\": stopping truncate due to conflicting lock request",
								fc_vacrel->relname)));
				return;
			}

			(void) WaitLatch(MyLatch,
							 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
							 VACUUM_TRUNCATE_LOCK_WAIT_INTERVAL,
							 WAIT_EVENT_VACUUM_TRUNCATE);
			ResetLatch(MyLatch);
		}

		/*
		 * 现在我们已经获得独占锁，查看在我们用非独占锁进行清理时关系是否增长。如果是，则放弃；新添加的页面大概包含不可删除的元组。
		 */
		fc_new_rel_pages = RelationGetNumberOfBlocks(fc_vacrel->rel);
		if (fc_new_rel_pages != fc_orig_rel_pages)
		{
			/*
			 * 注意：我们故意不在这里用新关系大小更新 vacrel->rel_pages。如果我们这样做，就相当于假设新页面是空的，这不太可能。保持数字不变就相当于假设新页面与现有页面的元组密度相同，这种情况不太可能。
			 */
			UnlockRelation(fc_vacrel->rel, AccessExclusiveLock);
			return;
		}

		/*
		 * 从末尾向后扫描以验证末尾页面实际上不包含元组。这是*必需的*，而不是可选的，因为其他后端在我们进行清理时可能已经向这些页面添加了元组。
		 */
		fc_new_rel_pages = fc_count_nondeletable_pages(fc_vacrel, &fc_lock_waiter_detected);
		fc_vacrel->blkno = fc_new_rel_pages;

		if (fc_new_rel_pages >= fc_orig_rel_pages)
		{
			/* 毕竟什么都做不了 */
			UnlockRelation(fc_vacrel->rel, AccessExclusiveLock);
			return;
		}

		/*
		 * 截断可以进行。
		 */
		RelationTruncate(fc_vacrel->rel, fc_new_rel_pages);

		/*
		 * 我们可以在完成截断后立即释放独占锁。
		 * 其他后端在处理 smgrtruncate 发出的 smgr 无效化之前无法安全访问该关系……但
		 * 一旦它们获得对该关系的锁，这应该是标准无效化处理的一部分。
		 */
		UnlockRelation(fc_vacrel->rel, AccessExclusiveLock);

		/*
		 * 更新统计信息。在这里，调整 rel_pages 是正确的
		 * 而无需调整 reltuples，因为元组计数并没有因为截断而改变。
		 */
		fc_vacrel->removed_pages += fc_orig_rel_pages - fc_new_rel_pages;
		fc_vacrel->rel_pages = fc_new_rel_pages;

		ereport(fc_vacrel->verbose ? INFO : DEBUG2,
				(errmsg("table \"%s\": truncated %u to %u pages",
						fc_vacrel->relname,
						fc_orig_rel_pages, fc_new_rel_pages)));
		fc_orig_rel_pages = fc_new_rel_pages;
	} while (fc_new_rel_pages > fc_vacrel->nonempty_pages && fc_lock_waiter_detected);
}

/*
 * 重新扫描结束页以验证它们是否（仍然）为空。
 *
 * 返回不可删除页面的数量（最后一个非空页面 + 1）。
 */
static BlockNumber fc_count_nondeletable_pages(LVRelState *fc_vacrel, bool *fc_lock_waiter_detected)
{
	BlockNumber fc_blkno;
	BlockNumber fc_prefetchedUntil;
	instr_time	fc_starttime;

	/* 如果我们检查冲突锁请求，则初始化开始时间 */
	INSTR_TIME_SET_CURRENT(fc_starttime);

	/*
	 * 从我们认为的关系结束处开始检查区块，并向后移动。
	 * （循环控制的奇怪编码是因为 blkno 是无符号的。）
	 * 为了加快扫描速度，我们一次预取几个区块
	 * 向前，以便操作系统级别的预读可以启动。
	 */
	fc_blkno = fc_vacrel->rel_pages;
	StaticAssertStmt((PREFETCH_SIZE & (PREFETCH_SIZE - 1)) == 0,
					 "prefetch size must be power of 2");
	fc_prefetchedUntil = InvalidBlockNumber;
	while (fc_blkno > fc_vacrel->nonempty_pages)
	{
		Buffer		fc_buf;
		Page		fc_page;
		OffsetNumber fc_offnum,
					fc_maxoff;
		bool		fc_hastup;

		/*
		 * 检查是否有其他进程请求我们关系上的锁。我们在这里持有一个 AccessExclusiveLock，
		 * 所以它们会在等待。我们每个 VACUUM_TRUNCATE_LOCK_CHECK_INTERVAL 只做这个一次，
		 * 并且我们只在每32个区块过了一次该间隔时检查，以
		 * 将系统调用和实际共享锁表查找的数量降到最低。
		 */
		if ((fc_blkno % 32) == 0)
		{
			instr_time	fc_currenttime;
			instr_time	fc_elapsed;

			INSTR_TIME_SET_CURRENT(fc_currenttime);
			fc_elapsed = fc_currenttime;
			INSTR_TIME_SUBTRACT(fc_elapsed, fc_starttime);
			if ((INSTR_TIME_GET_MICROSEC(fc_elapsed) / 1000)
				>= VACUUM_TRUNCATE_LOCK_CHECK_INTERVAL)
			{
				if (LockHasWaitersRelation(fc_vacrel->rel, AccessExclusiveLock))
				{
					ereport(fc_vacrel->verbose ? INFO : DEBUG2,
							(errmsg("table \"%s\": suspending truncate due to conflicting lock request",
									fc_vacrel->relname)));

					*fc_lock_waiter_detected = true;
					return fc_blkno;
				}
				fc_starttime = fc_currenttime;
			}
		}

		/*
		 * 我们在这里不插入 vacuum 延迟点，因为我们对表
		 * 具有独占锁，而我们希望尽可能短时间内保持这个锁。
		 * 不过我们仍然需要检查中断。
		 */
		CHECK_FOR_INTERRUPTS();

		fc_blkno--;

		/* 如果我们还没有预取这一批，现在就去做。 */
		if (fc_prefetchedUntil > fc_blkno)
		{
			BlockNumber fc_prefetchStart;
			BlockNumber fc_pblkno;

			fc_prefetchStart = fc_blkno & ~(PREFETCH_SIZE - 1);
			for (fc_pblkno = fc_prefetchStart; fc_pblkno <= fc_blkno; fc_pblkno++)
			{
				PrefetchBuffer(fc_vacrel->rel, MAIN_FORKNUM, fc_pblkno);
				CHECK_FOR_INTERRUPTS();
			}
			fc_prefetchedUntil = fc_prefetchStart;
		}

		fc_buf = ReadBufferExtended(fc_vacrel->rel, MAIN_FORKNUM, fc_blkno, RBM_NORMAL,
								 fc_vacrel->bstrategy);

		/* 在此阶段我们只需共享访问缓冲区 */
		LockBuffer(fc_buf, BUFFER_LOCK_SHARE);

		fc_page = BufferGetPage(fc_buf);

		if (PageIsNew(fc_page) || PageIsEmpty(fc_page))
		{
			UnlockReleaseBuffer(fc_buf);
			continue;
		}

		fc_hastup = false;
		fc_maxoff = PageGetMaxOffsetNumber(fc_page);
		for (fc_offnum = FirstOffsetNumber;
			 fc_offnum <= fc_maxoff;
			 fc_offnum = OffsetNumberNext(fc_offnum))
		{
			ItemId		fc_itemid;

			fc_itemid = PageGetItemId(fc_page, fc_offnum);

			/*
			 * 注意：任何未使用的项目都应视为保留
			 * 此页面的理由。甚至一个 LP_DEAD 项目也会使截断变得不安全，因为
			 * 我们必须没有清理掉它的索引条目。
			 */
			if (ItemIdIsUsed(fc_itemid))
			{
				fc_hastup = true;
				break;			/* 可以停止扫描 */
			}
		}						/* 沿页面扫描 */

		UnlockReleaseBuffer(fc_buf);

		/* 如果我们在这里找到一个元组，则完成扫描 */
		if (fc_hastup)
			return fc_blkno + 1;
	}

	/*
	 * 如果我们从循环中退出，所有先前认为是空的
	 * 页面仍然是空的；我们不必再查看最后已知的非空
	 * 页面。
	 */
	return fc_vacrel->nonempty_pages;
}

/*
 * 返回 VACUUM 应该分配空间存储的死 TID 的数量，
 * 鉴于大小为 vacrel->rel_pages 的堆关系，并给定当前
 * maintenance_work_mem 设置（或当前 autovacuum_work_mem 设置，
 * 适用时）。
 *
 * 请参阅本文件开头的注释以获取理由。
 */
static int fc_dead_items_max_items(LVRelState *fc_vacrel)
{
	int64		fc_max_items;
	int			fc_vac_work_mem = IsAutoVacuumWorkerProcess() &&
	autovacuum_work_mem != -1 ?
	autovacuum_work_mem : maintenance_work_mem;

	if (fc_vacrel->nindexes > 0)
	{
		BlockNumber fc_rel_pages = fc_vacrel->rel_pages;

		fc_max_items = MAXDEADITEMS(fc_vac_work_mem * 1024L);
		fc_max_items = Min(fc_max_items, INT_MAX);
		fc_max_items = Min(fc_max_items, MAXDEADITEMS(MaxAllocSize));

		/* 这里有好奇的编码以确保乘法不会溢出 */
		if ((BlockNumber) (fc_max_items / MaxHeapTuplesPerPage) > fc_rel_pages)
			fc_max_items = fc_rel_pages * MaxHeapTuplesPerPage;

		/* 如果 maintenance_work_mem 较小，则保持理智 */
		fc_max_items = Max(fc_max_items, MaxHeapTuplesPerPage);
	}
	else
	{
		/* 单遍情况一次只存储单个堆页面的 TID */
		fc_max_items = MaxHeapTuplesPerPage;
	}

	return (int) fc_max_items;
}

/*
 * 分配 dead_items（可以使用 palloc，或者在动态共享内存中）。
 * 为调用者设置 vacrel 中的 dead_items。
 *
 * 还处理在需要时在 DSM 中分配 dead_items 的并行初始化。
 */
static void fc_dead_items_alloc(LVRelState *fc_vacrel, int fc_nworkers)
{
	VacDeadItems *fc_dead_items;
	int			fc_max_items;

	fc_max_items = fc_dead_items_max_items(fc_vacrel);
	Assert(fc_max_items >= MaxHeapTuplesPerPage);

	/*
	 * 初始化并行 vacuum 的状态。目前，仅可以为一个索引使用一个工作线程，
	 * 所以只有在表上至少有两个索引时，我们才会调用并行性。
	 */
	if (fc_nworkers >= 0 && fc_vacrel->nindexes > 1 && fc_vacrel->do_index_vacuuming)
	{
		/*
		 * 由于并行工作线程无法访问临时表中的数据，
		 * 所以我们不能对它们执行并行 vacuum。
		 */
		if (RelationUsesLocalBuffers(fc_vacrel->rel))
		{
			/*
			 * 仅当用户明确尝试对临时表执行并行 vacuum 时才发出警告。
			 */
			if (fc_nworkers > 0)
				ereport(WARNING,
						(errmsg("disabling parallel option of vacuum on \"%s\" --- cannot vacuum temporary tables in parallel",
								fc_vacrel->relname)));
		}
		else
			fc_vacrel->pvs = parallel_vacuum_init(fc_vacrel->rel, fc_vacrel->indrels,
											   fc_vacrel->nindexes, fc_nworkers,
											   fc_max_items,
											   fc_vacrel->verbose ? INFO : DEBUG2,
											   fc_vacrel->bstrategy);

		/* 如果启动了并行模式，dead_items 空间在 DSM 中分配 */
		if (ParallelVacuumIsActive(fc_vacrel))
		{
			fc_vacrel->dead_items = parallel_vacuum_get_dead_items(fc_vacrel->pvs);
			return;
		}
	}

	/* 串行 VACUUM 情况 */
	fc_dead_items = (VacDeadItems *) palloc(vac_max_items_to_alloc_size(fc_max_items));
	fc_dead_items->max_items = fc_max_items;
	fc_dead_items->num_items = 0;

	fc_vacrel->dead_items = fc_dead_items;
}

/*
 * 对在 dead_items_alloc 中分配的资源进行清理
 */
static void fc_dead_items_cleanup(LVRelState *fc_vacrel)
{
	if (!ParallelVacuumIsActive(fc_vacrel))
	{
		/* 这里不要使用 pfree */
		return;
	}

	/* 结束并行模式 */
	parallel_vacuum_end(fc_vacrel->pvs, fc_vacrel->indstats);
	fc_vacrel->pvs = NULL;
}

/*
 * 检查给定页面中的每个元组是否对所有当前和未来的事务可见。
 * 同时返回 visibility_cutoff_xid，它是可见元组中最高的 xmin。
 * 如果该页面上的每个元组都是冻结的，则将 *all_frozen 设置为 true。
 *
 * 这是 lazy_scan_prune() 的简化版本。如果您在这里更改任何内容，请确保
 * 一切保持同步。请注意，断言调用我们以确保所有人仍然一致。
 * 确保避免在这里引入新的副作用。
 */
static bool fc_heap_page_is_all_visible(LVRelState *fc_vacrel, Buffer fc_buf,
						 TransactionId *fc_visibility_cutoff_xid,
						 bool *fc_all_frozen)
{
	Page		fc_page = BufferGetPage(fc_buf);
	BlockNumber fc_blockno = BufferGetBlockNumber(fc_buf);
	OffsetNumber fc_offnum,
				fc_maxoff;
	bool		fc_all_visible = true;

	*fc_visibility_cutoff_xid = InvalidTransactionId;
	*fc_all_frozen = true;

	fc_maxoff = PageGetMaxOffsetNumber(fc_page);
	for (fc_offnum = FirstOffsetNumber;
		 fc_offnum <= fc_maxoff && fc_all_visible;
		 fc_offnum = OffsetNumberNext(fc_offnum))
	{
		ItemId		fc_itemid;
		HeapTupleData fc_tuple;

		/*
		 * 设置偏移量以便我们可以在处理此元组时显示它
		 * 以及发生的任何错误。
		 */
		fc_vacrel->offnum = fc_offnum;
		fc_itemid = PageGetItemId(fc_page, fc_offnum);

		/* 未使用或重定向的行指针无关紧要 */
		if (!ItemIdIsUsed(fc_itemid) || ItemIdIsRedirected(fc_itemid))
			continue;

		ItemPointerSet(&(fc_tuple.t_self), fc_blockno, fc_offnum);

		/*
		 * 死亡行指针可能有指向它们的索引指针。因此
		 * 它们不能被视为可见的
		 */
		if (ItemIdIsDead(fc_itemid))
		{
			fc_all_visible = false;
			*fc_all_frozen = false;
			break;
		}

		Assert(ItemIdIsNormal(fc_itemid));

		fc_tuple.t_data = (HeapTupleHeader) PageGetItem(fc_page, fc_itemid);
		fc_tuple.t_len = ItemIdGetLength(fc_itemid);
		fc_tuple.t_tableOid = RelationGetRelid(fc_vacrel->rel);

		switch (HeapTupleSatisfiesVacuum(&fc_tuple, fc_vacrel->OldestXmin, fc_buf))
		{
			case HEAPTUPLE_LIVE:
				{
					TransactionId fc_xmin;

					/* 检查 lazy_scan_prune 中的注释。 */
					if (!HeapTupleHeaderXminCommitted(fc_tuple.t_data))
					{
						fc_all_visible = false;
						*fc_all_frozen = false;
						break;
					}

					/*
					 * 插入者肯定已提交。但它是否足够旧，以至于每个人都将其视为已提交？
					 */
					fc_xmin = HeapTupleHeaderGetXmin(fc_tuple.t_data);
					if (!TransactionIdPrecedes(fc_xmin, fc_vacrel->OldestXmin))
					{
						fc_all_visible = false;
						*fc_all_frozen = false;
						break;
					}

					/* 跟踪页面上的最新 xmin。 */
					if (TransactionIdFollows(fc_xmin, *fc_visibility_cutoff_xid))
						*fc_visibility_cutoff_xid = fc_xmin;

					/* 检查这个元组是否已经被冻结 */
					if (fc_all_visible && *fc_all_frozen &&
						heap_tuple_needs_eventual_freeze(fc_tuple.t_data))
						*fc_all_frozen = false;
				}
				break;

			case HEAPTUPLE_DEAD:
			case HEAPTUPLE_RECENTLY_DEAD:
			case HEAPTUPLE_INSERT_IN_PROGRESS:
			case HEAPTUPLE_DELETE_IN_PROGRESS:
				{
					fc_all_visible = false;
					*fc_all_frozen = false;
					break;
				}
			default:
				elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
				break;
		}
	}							/* 沿页面扫描 */

	/* 一旦处理完给定页面，就清除偏移量信息。 */
	fc_vacrel->offnum = InvalidOffsetNumber;

	return fc_all_visible;
}

/*
 * 如果统计信息是准确的，则更新 pg_class 中的索引统计信息。
 */
static void fc_update_relstats_all_indexes(LVRelState *fc_vacrel)
{
	Relation   *fc_indrels = fc_vacrel->indrels;
	int			fc_nindexes = fc_vacrel->nindexes;
	IndexBulkDeleteResult **fc_indstats = fc_vacrel->indstats;

	Assert(fc_vacrel->do_index_cleanup);

	for (int fc_idx = 0; fc_idx < fc_nindexes; fc_idx++)
	{
		Relation	fc_indrel = fc_indrels[fc_idx];
		IndexBulkDeleteResult *fc_istat = fc_indstats[fc_idx];

		if (fc_istat == NULL || fc_istat->estimated_count)
			continue;

		/* 更新索引统计信息 */
		vac_update_relstats(fc_indrel,
							fc_istat->num_pages,
							fc_istat->num_index_tuples,
							0,
							false,
							InvalidTransactionId,
							InvalidMultiXactId,
							NULL, NULL, false);
	}
}

/*
 * 出现 vacuum 错误时的错误上下文回调。索引阶段的错误
 * 上下文消息应与并行 vacuum 中设置的消息匹配。
 * 如果您更改该阶段的此函数，也请更改 parallel_vacuum_error_callback()。
 */
static void fc_vacuum_error_callback(void *fc_arg)
{
	LVRelState *fc_errinfo = fc_arg;

	switch (fc_errinfo->phase)
	{
		case VACUUM_ERRCB_PHASE_SCAN_HEAP:
			if (BlockNumberIsValid(fc_errinfo->blkno))
			{
				if (OffsetNumberIsValid(fc_errinfo->offnum))
					errcontext("while scanning block %u offset %u of relation \"%s.%s\"",
							   fc_errinfo->blkno, fc_errinfo->offnum, fc_errinfo->relnamespace, fc_errinfo->relname);
				else
					errcontext("while scanning block %u of relation \"%s.%s\"",
							   fc_errinfo->blkno, fc_errinfo->relnamespace, fc_errinfo->relname);
			}
			else
				errcontext("while scanning relation \"%s.%s\"",
						   fc_errinfo->relnamespace, fc_errinfo->relname);
			break;

		case VACUUM_ERRCB_PHASE_VACUUM_HEAP:
			if (BlockNumberIsValid(fc_errinfo->blkno))
			{
				if (OffsetNumberIsValid(fc_errinfo->offnum))
					errcontext("while vacuuming block %u offset %u of relation \"%s.%s\"",
							   fc_errinfo->blkno, fc_errinfo->offnum, fc_errinfo->relnamespace, fc_errinfo->relname);
				else
					errcontext("while vacuuming block %u of relation \"%s.%s\"",
							   fc_errinfo->blkno, fc_errinfo->relnamespace, fc_errinfo->relname);
			}
			else
				errcontext("while vacuuming relation \"%s.%s\"",
						   fc_errinfo->relnamespace, fc_errinfo->relname);
			break;

		case VACUUM_ERRCB_PHASE_VACUUM_INDEX:
			errcontext("while vacuuming index \"%s\" of relation \"%s.%s\"",
					   fc_errinfo->indname, fc_errinfo->relnamespace, fc_errinfo->relname);
			break;

		case VACUUM_ERRCB_PHASE_INDEX_CLEANUP:
			errcontext("while cleaning up index \"%s\" of relation \"%s.%s\"",
					   fc_errinfo->indname, fc_errinfo->relnamespace, fc_errinfo->relname);
			break;

		case VACUUM_ERRCB_PHASE_TRUNCATE:
			if (BlockNumberIsValid(fc_errinfo->blkno))
				errcontext("while truncating relation \"%s.%s\" to %u blocks",
						   fc_errinfo->relnamespace, fc_errinfo->relname, fc_errinfo->blkno);
			break;

		case VACUUM_ERRCB_PHASE_UNKNOWN:
		default:
			return;				/* 什么都不做；errinfo 可能未初始化 */
	}
}

/*
 * 更新 vacuum 错误回调所需的信息。这还保存
 * 当前信息，可以通过 restore_vacuum_error_info 进行恢复。
 */
static void fc_update_vacuum_error_info(LVRelState *fc_vacrel, LVSavedErrInfo *fc_saved_vacrel,
						 int fc_phase, BlockNumber fc_blkno, OffsetNumber fc_offnum)
{
	if (fc_saved_vacrel)
	{
		fc_saved_vacrel->offnum = fc_vacrel->offnum;
		fc_saved_vacrel->blkno = fc_vacrel->blkno;
		fc_saved_vacrel->phase = fc_vacrel->phase;
	}

	fc_vacrel->blkno = fc_blkno;
	fc_vacrel->offnum = fc_offnum;
	fc_vacrel->phase = fc_phase;
}

/*
 * 恢复通过先前调用 update_vacuum_error_info 保存的 vacuum 信息。
 */
static void fc_restore_vacuum_error_info(LVRelState *fc_vacrel,
						  const LVSavedErrInfo *fc_saved_vacrel)
{
	fc_vacrel->blkno = fc_saved_vacrel->blkno;
	fc_vacrel->offnum = fc_saved_vacrel->offnum;
	fc_vacrel->phase = fc_saved_vacrel->phase;
}
